import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { Link, useLocation, useNavigate, useParams } from 'react-router-dom'; import { useAccount, useComment, useSubscribe } from '@bitsocial/bitsocial-react-hooks'; import { isAllView, isCatalogView, isModView, isModQueueView, isPendingPostView, isPostPageView, isSubscriptionsView } from '../../lib/utils/view-utils'; import { usePostPageNumber } from '../../hooks/use-post-page-number'; import { useDirectories, useDirectoryByAddress } from '../../hooks/use-directories'; import { useAccountCommunityAddresses } from '../../hooks/use-account-community-addresses'; import { useFilteredDirectoryAddresses } from '../../hooks/use-filtered-directory-addresses'; import { getBoardPath, isDirectoryBoard } from '../../lib/utils/route-utils'; import { useResolvedCommunityAddress } from '../../hooks/use-resolved-community-address'; import useSafeAccountComment from '../../hooks/use-safe-account-comment'; import useHiddenCatalogThreads from '../../hooks/use-hidden-catalog-threads'; 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 useHiddenCatalogThreadsStore from '../../stores/use-hidden-catalog-threads-store'; import useSortingStore from '../../stores/use-sorting-store'; import useAllFeedFilterStore from '../../stores/use-all-feed-filter-store'; import useModQueueStore from '../../stores/use-mod-queue-store'; import useFeedViewSettingsStore from '../../stores/use-feed-view-settings-store'; import useThreadLiveUpdatesStore from '../../stores/use-thread-live-updates-store'; import useCountLinksInReplies from '../../hooks/use-count-links-in-replies'; import useIsMobile from '../../hooks/use-is-mobile'; import useTimeFilter from '../../hooks/use-time-filter'; import CatalogFilters from '../catalog-filters'; import CatalogSearch from '../catalog-search'; import Tooltip from '../tooltip'; import { ModQueueButton } from '../../views/mod-queue/mod-queue'; import { isCommentArchived } from '../../lib/utils/comment-moderation-utils'; import { getSearchWithTimeFilter, getTimeFilterOptionLabel } from '../../lib/utils/time-filter-utils'; import styles from './board-buttons.module.css'; import capitalize from 'lodash/capitalize'; interface BoardButtonsProps { address?: string | undefined; isInAllView?: boolean; isInCatalogView?: boolean; isInSubscriptionsView?: boolean; isInModView?: boolean; isInModQueueView?: boolean; isTopbar?: boolean; } const EMPTY_COMMUNITY_ADDRESSES: string[] = []; const getMultiboardPath = ({ isInAllView, isInCatalogView, isInSubscriptionsView, isInModView, }: Pick) => { if (isInAllView) { return isInCatalogView ? '/all/catalog' : '/all'; } if (isInSubscriptionsView) { return isInCatalogView ? '/subs/catalog' : '/subs'; } if (isInModView) { return isInCatalogView ? '/mod/catalog' : '/mod'; } return null; }; export const CatalogButton = ({ address, isInAllView, isInSubscriptionsView, isInModView }: BoardButtonsProps) => { const { t } = useTranslation(); const location = useLocation(); const directories = useDirectories(); const { timeFilterValue } = useTimeFilter(); const createCatalogLink = () => { const multiboardPath = getMultiboardPath({ isInAllView, isInCatalogView: true, isInSubscriptionsView, isInModView }); if (multiboardPath) { return { pathname: multiboardPath, search: getSearchWithTimeFilter(location.search, timeFilterValue), }; } let boardPath = ''; if (address) { boardPath = getBoardPath(address, directories); } else if (Array.isArray(directories) && directories.length > 0 && directories[0]?.address) { boardPath = getBoardPath(directories[0].address, directories); } return `/${boardPath}/catalog`; }; return ( {t('catalog')} ); }; export const ArchiveButton = ({ address, isInAllView, isInSubscriptionsView, isInModView }: BoardButtonsProps) => { const { t } = useTranslation(); const navigate = useNavigate(); const params = useParams(); const directories = useDirectories(); const isInvalidArchiveContext = isInAllView || isInSubscriptionsView || isInModView; const boardIdentifier = params.boardIdentifier; const archiveBoardIdentifier = address ? getBoardPath(address, directories) : boardIdentifier ? getBoardPath(boardIdentifier, directories) : ''; const archivePath = archiveBoardIdentifier ? `/${archiveBoardIdentifier}/archive` : ''; if (isInvalidArchiveContext || !archivePath) { return null; } return ( ); }; const SubscribeButton = ({ address }: BoardButtonsProps) => { const { t } = useTranslation(); const { subscribed, subscribe, unsubscribe } = useSubscribe({ communityAddress: address }); return ( ); }; export const ReturnButton = ({ address, isInAllView, isInSubscriptionsView, isInModView, isInModQueueView }: BoardButtonsProps) => { const { t } = useTranslation(); const location = useLocation(); const params = useParams(); const directories = useDirectories(); const { timeFilterValue } = useTimeFilter(); const createReturnLink = () => { if (isInModQueueView) { // If in mod queue view, return to /mod or /:boardIdentifier if (params?.boardIdentifier) { return `/${params.boardIdentifier}`; } return `/mod`; } const multiboardPath = getMultiboardPath({ isInAllView, isInCatalogView: false, isInSubscriptionsView, isInModView }); if (multiboardPath) { return { pathname: multiboardPath, search: getSearchWithTimeFilter(location.search, timeFilterValue, { removeKeys: ['q'] }), }; } let boardPath = ''; if (address) { boardPath = getBoardPath(address, directories); } else if (Array.isArray(directories) && directories.length > 0 && directories[0]?.address) { boardPath = getBoardPath(directories[0].address, directories); } return `/${boardPath}`; }; return ( {t('return')} ); }; const VoteButton = () => { const { t } = useTranslation(); const params = useParams(); const directories = useDirectories(); // Get the boardIdentifier from params (try boardIdentifier first, then communityAddress for backward compatibility) const boardIdentifier = params.boardIdentifier; // Only render the vote button if we're on a directory board route if (!boardIdentifier || !isDirectoryBoard(boardIdentifier, directories)) { return null; } const values = { boardIdentifier }; const message = `${t('vote_button_unavailable_intro', values)}\n\n${t('vote_button_unavailable_outro', values)}`; return ( ); }; export const RefreshButton = () => { const { t } = useTranslation(); const reset = useFeedResetStore((state) => state.reset); return ( ); }; const HiddenCatalogThreadsToggle = ({ address, isInAllView = false, isInCatalogView = false, isInSubscriptionsView = false, isInModView = false, isMobilePlacement = false, }: BoardButtonsProps & { isMobilePlacement?: boolean }) => { const account = useAccount(); const accountCommunityAddresses = useAccountCommunityAddresses(); const filteredDirectoryAddresses = useFilteredDirectoryAddresses(); const sortType = useSortingStore((state) => state.sortType); const toggleShownScopeKey = useHiddenCatalogThreadsStore((state) => state.toggleShownScopeKey); const communityAddresses = useMemo(() => { if (isInAllView) { return filteredDirectoryAddresses; } if (isInSubscriptionsView) { return account?.subscriptions?.filter(Boolean) || EMPTY_COMMUNITY_ADDRESSES; } if (isInModView) { return accountCommunityAddresses; } return address ? [address] : EMPTY_COMMUNITY_ADDRESSES; }, [account?.subscriptions, accountCommunityAddresses, address, filteredDirectoryAddresses, isInAllView, isInModView, isInSubscriptionsView]); const { hiddenCatalogThreads, isLoadingHiddenCatalogThreads, scopeKey } = useHiddenCatalogThreads({ communityAddresses, sortType: sortType === 'new' ? 'new' : 'active', }); const storedHiddenThreadsCount = useHiddenCatalogThreadsStore((state) => state.scopeHiddenThreadsCounts[scopeKey] || 0); const hiddenThreadsCount = Math.max(hiddenCatalogThreads.length, storedHiddenThreadsCount); const requestedShowHiddenThreads = useHiddenCatalogThreadsStore((state) => state.shownScopeKey === scopeKey); const showHiddenThreads = requestedShowHiddenThreads && (hiddenThreadsCount > 0 || isLoadingHiddenCatalogThreads); if (!isInCatalogView || hiddenThreadsCount === 0) { return null; } return ( — Hidden threads: {hiddenThreadsCount} [ ] ); }; export const UpdateButton = () => { const { t } = useTranslation(); const requestUpdate = useThreadLiveUpdatesStore((state) => state.requestUpdate); return ( ); }; export const AutoButton = () => { const { t } = useTranslation(); const isMobile = useIsMobile(); const autoUpdateEnabled = useThreadLiveUpdatesStore((state) => state.enabled); const setAutoUpdateEnabled = useThreadLiveUpdatesStore((state) => state.setEnabled); return ( ); }; export const BottomButton = () => { const { t } = useTranslation(); const scrollToBottom = () => { window.scrollTo({ top: document.documentElement.scrollHeight, behavior: 'instant' }); }; return ( ); }; export const TopButton = () => { const { t } = useTranslation(); const scrollToTop = () => { window.scrollTo({ top: 0, left: 0, behavior: 'instant' }); }; return ( ); }; const SortOptions = () => { const { t } = useTranslation(); const { sortType, setSortType } = useSortingStore(); const handleSortChange = (event: React.ChangeEvent) => { const type = event.target.value as 'active' | 'new' | 'replyCount'; setSortType(type); }; return ( <> {t('sort_by')} ); }; const ImageSizeOptions = () => { const { t } = useTranslation(); const { imageSize, setImageSize } = useCatalogStyleStore(); return ( <> {t('image_size')}:  ); }; const MAX_ALERT_THRESHOLD = 10000; // Maximum threshold value in minutes (~166 hours) const ModQueueAlertThreshold = () => { const { t } = useTranslation(); const { alertThresholdValue, alertThresholdUnit, setAlertThreshold } = useModQueueStore(); const handleThresholdChange = (e: React.ChangeEvent) => { const inputValue = e.target.value.trim(); // Handle empty input - allow it temporarily for better UX if (inputValue === '') { return; } // Parse safely const parsedValue = parseInt(inputValue, 10); // Default to 1 if invalid or NaN if (isNaN(parsedValue) || parsedValue < 1) { setAlertThreshold(1, alertThresholdUnit); return; } // Convert to minutes for clamping, then convert back to current unit const valueInMinutes = alertThresholdUnit === 'hours' ? parsedValue * 60 : parsedValue; const clampedMinutes = Math.min(valueInMinutes, MAX_ALERT_THRESHOLD); const finalValue = alertThresholdUnit === 'hours' ? Math.round(clampedMinutes / 60) : clampedMinutes; setAlertThreshold(finalValue, alertThresholdUnit); }; const handleThresholdBlur = (e: React.FocusEvent) => { const inputValue = e.target.value.trim(); // If empty or invalid, restore to current value or default to 1 if (inputValue === '' || isNaN(parseInt(inputValue, 10))) { const safeValue = alertThresholdValue >= 1 ? alertThresholdValue : 1; setAlertThreshold(safeValue, alertThresholdUnit); } }; return (
); }; const ModQueueViewSelector = () => { const { t } = useTranslation(); const { viewMode, setViewMode } = useModQueueStore(); return (
); }; const ShowOPCommentOption = () => { const { t } = useTranslation(); const { showOPComment, setShowOPComment } = useCatalogStyleStore(); return ( <> {t('show_op_comment')}:  ); }; const TimeFilter = ({ isInAllView, isInCatalogView, isInSubscriptionsView, isInModView, isTopbar = false }: BoardButtonsProps) => { const { t } = useTranslation(); const location = useLocation(); const navigate = useNavigate(); const { lastVisitTimeFilterName, timeFilterValue, timeFilterValues } = useTimeFilter(); const multiboardPath = getMultiboardPath({ isInAllView, isInCatalogView, isInSubscriptionsView, isInModView }); if (!multiboardPath) { return null; } const changeTimeFilter = (event: React.ChangeEvent) => { navigate({ pathname: multiboardPath, search: getSearchWithTimeFilter(location.search, event.target.value), }); }; return ( <> {!isTopbar && ( <> {t('filter')}:  )} ); }; const AllFeedFilter = () => { const { t } = useTranslation(); const { filter, setFilter } = useAllFeedFilterStore(); return ( <> {t('show')} ); }; export const MobileAllFeedFilter = () => (

); export const MobileBoardButtons = () => { const { t } = useTranslation(); const params = useParams(); const location = useLocation(); const isInAllView = isAllView(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, useParams()); const isInModView = isModView(location.pathname); const isInModQueueView = isModQueueView(location.pathname); const accountComment = useSafeAccountComment({ commentIndex: params?.accountCommentIndex }); const resolvedAddress = useResolvedCommunityAddress(); const communityAddress = resolvedAddress || accountComment?.communityAddress; const { filteredCount, searchText } = useCatalogFiltersStore(); const enableInfiniteScroll = useFeedViewSettingsStore((state) => state.enableInfiniteScroll); const isMultiboard = isInAllView || isInSubscriptionsView || isInModView; const showTimeFilter = isMultiboard; const effectiveInfiniteScroll = isMultiboard || enableInfiniteScroll; const showBottomButton = !effectiveInfiniteScroll; // Check if we should show the vote button (only for directory boards) const directories = useDirectories(); const boardIdentifier = params.boardIdentifier; const showVoteButton = boardIdentifier && isDirectoryBoard(boardIdentifier, directories); return (
{isInPostView || isInPendingPostPage ? ( <> {showBottomButton && }
) : isInModQueueView ? ( <> ) : isInCatalogView ? ( <> {showBottomButton && } {searchText ? ( {' '} - {t('search_results_for')}: {searchText} ) : ( filteredCount > 0 && ( {' '} - {t('filtered_threads')}: {filteredCount} ) )} {(isInAllView || showTimeFilter || isInCatalogView) && ( <>
{(isInAllView || showTimeFilter) && (
{isInAllView && }{' '} {showTimeFilter && ( )}
)} {isInCatalogView && (
)}
)} ) : ( <> {showBottomButton && }
{showVoteButton && } {!(isInAllView || isInSubscriptionsView || isInModView) && } {!(isInAllView || isInSubscriptionsView) && }
{showTimeFilter && ( <>
)} )}
); }; export const PostPageStats = () => { const { t } = useTranslation(); const params = useParams(); const location = useLocation(); const autoUpdateEnabled = useThreadLiveUpdatesStore((state) => state.enabled); const commentCid = params?.commentCid as string | undefined; const resolvedAddress = useResolvedCommunityAddress(); const accountComment = useSafeAccountComment({ commentIndex: params?.accountCommentIndex }); const communityAddress = resolvedAddress || accountComment?.communityAddress; const comment = useComment({ commentCid, autoUpdate: autoUpdateEnabled }); const postCid = comment?.postCid ?? commentCid; const post = useComment({ commentCid: postCid, autoUpdate: autoUpdateEnabled }); const archived = isCommentArchived(post); const { closed, pinned, replyCount } = post || {}; const linkCount = useCountLinksInReplies(post); const directoryEntry = useDirectoryByAddress(communityAddress); const requirePostLinkIsMedia = directoryEntry?.features?.requirePostLinkIsMedia === true; const isThreadView = isPostPageView(location.pathname, params); const pageNumber = usePostPageNumber({ communityAddress, postCid, enabled: isThreadView, }); const displayReplyCount = replyCount !== undefined ? replyCount.toString() : '?'; const replyCountTooltip = replyCount !== undefined ? capitalize(t('replies')) : t('loading'); return ( {pinned && `${capitalize(t('sticky'))} / `} {archived && `${capitalize(t('archived'))} / `} {closed && `${capitalize(t('closed'))} / `} {displayReplyCount} /{' '} {linkCount?.toString()} {isThreadView && ( <> {' '} / {pageNumber?.toString() ?? '?'} )} ); }; export const DesktopBoardButtons = () => { const { t } = useTranslation(); const params = useParams(); const location = useLocation(); const accountComment = useSafeAccountComment({ commentIndex: params?.accountCommentIndex }); const resolvedAddress = useResolvedCommunityAddress(); const communityAddress = resolvedAddress || accountComment?.communityAddress; const isInCatalogView = isCatalogView(location.pathname, params); const isInAllView = isAllView(location.pathname); const isInPendingPostPage = isPendingPostView(location.pathname, params); const isInPostView = isPostPageView(location.pathname, params); const isInSubscriptionsView = isSubscriptionsView(location.pathname, useParams()); const isInModView = isModView(location.pathname); const isInModQueueView = isModQueueView(location.pathname); const { filteredCount, searchText } = useCatalogFiltersStore(); const enableInfiniteScroll = useFeedViewSettingsStore((state) => state.enableInfiniteScroll); const isMultiboard = isInAllView || isInSubscriptionsView || isInModView; const showTimeFilter = isMultiboard; const effectiveInfiniteScroll = isMultiboard || enableInfiniteScroll; const showBottomButton = (isInCatalogView || isInPostView || isInPendingPostPage) && !effectiveInfiniteScroll; // Check if we should show the vote button (only for directory boards) const directories = useDirectories(); const boardIdentifier = params.boardIdentifier; const showVoteButton = boardIdentifier && isDirectoryBoard(boardIdentifier, directories); return ( <>
{isInPostView || isInPendingPostPage ? ( <> [] [ ] {showBottomButton && ( <> {' '} [] )}{' '} [] [] ) : isInModQueueView ? ( <> [ ] [] ) : ( <> {isInCatalogView ? ( <> [] {!(isInAllView || isInSubscriptionsView || isInModView) && ( <> {' '} [] )}{' '} ) : ( <> [] {!(isInAllView || isInSubscriptionsView || isInModView) && ( <> {' '} [] )}{' '} )} {showBottomButton && ( <> {' '} [] )}{' '} [] {isInCatalogView && ( <> {' '} )} {!(isInAllView || isInSubscriptionsView) && ( <> {' '} )} {isInCatalogView && searchText ? ( {' '} - {t('search_results_for')}: {searchText} ) : ( isInCatalogView && filteredCount > 0 && ( {' '} - {t('filtered_threads')}: {filteredCount} ) )} {isInCatalogView && ( <> )} {isInAllView && } {showTimeFilter && ( )} {showVoteButton && ( <> [] )} {showVoteButton && !(isInAllView || isInSubscriptionsView || isInModView) && ' '} {!(isInAllView || isInSubscriptionsView || isInModView) && ( <> [] )}{' '} {isInCatalogView && ( <> [] )} )}
); }; const SearchOPsBar = () => { const { t } = useTranslation(); const navigate = useNavigate(); const params = useParams(); const location = useLocation(); const isInAllView = isAllView(location.pathname); const isInSubscriptionsView = isSubscriptionsView(location.pathname, useParams()); const isInModView = isModView(location.pathname); const directories = useDirectories(); const resolvedAddress = useResolvedCommunityAddress(); const boardPath = resolvedAddress ? getBoardPath(resolvedAddress, directories) : params?.boardIdentifier || params?.communityAddress; const handleSearch = (event: React.KeyboardEvent) => { if (event.key === 'Enter') { const searchQuery = (event.target as HTMLInputElement).value.trim(); if (searchQuery) { const params = new URLSearchParams(location.search); params.set('q', searchQuery); const search = `?${params.toString()}`; if (isInAllView) { navigate({ pathname: '/all/catalog', search }); } else if (isInSubscriptionsView) { navigate({ pathname: '/subs/catalog', search }); } else if (isInModView) { navigate({ pathname: '/mod/catalog', search }); } else { navigate(`/${boardPath}/catalog?q=${encodeURIComponent(searchQuery)}`); } } } }; return ; };