import { useTranslation } from 'react-i18next'; import { Link, useLocation, useNavigate, useParams } from 'react-router-dom'; import { useAccountComment, useComment, useSubscribe } from '@plebbit/plebbit-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 { getBoardPath, isDirectoryBoard } from '../../lib/utils/route-utils'; import { useResolvedSubplebbitAddress } from '../../hooks/use-resolved-subplebbit-address'; 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 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 useCountLinksInReplies from '../../hooks/use-count-links-in-replies'; import useIsMobile from '../../hooks/use-is-mobile'; import CatalogFilters from '../catalog-filters'; import CatalogSearch from '../catalog-search'; import Tooltip from '../tooltip'; import { ModQueueButton } from '../../views/mod-queue/mod-queue'; 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; } export const CatalogButton = ({ address, isInAllView, isInSubscriptionsView, isInModView }: BoardButtonsProps) => { const { t } = useTranslation(); const params = useParams(); const directories = useDirectories(); const createCatalogLink = () => { if (isInAllView) return `/all/catalog`; if (isInSubscriptionsView) return `/subs/catalog`; if (isInModView) return `/mod/catalog`; 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 ( ); }; const SubscribeButton = ({ address }: BoardButtonsProps) => { const { t } = useTranslation(); const { subscribed, subscribe, unsubscribe } = useSubscribe({ subplebbitAddress: address }); return ( ); }; export const ReturnButton = ({ address, isInAllView, isInSubscriptionsView, isInModView, isInModQueueView }: BoardButtonsProps) => { const { t } = useTranslation(); const params = useParams(); const directories = useDirectories(); const createReturnLink = () => { if (isInAllView) return `/all`; if (isInSubscriptionsView) return `/subs`; if (isInModQueueView) { // If in mod queue view, return to /mod or /:boardIdentifier if (params?.boardIdentifier) { return `/${params.boardIdentifier}`; } return `/mod`; } if (isInModView) return `/mod`; 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 ( ); }; const VoteButton = () => { const { t } = useTranslation(); const params = useParams(); const directories = useDirectories(); // Get the boardIdentifier from params (try boardIdentifier first, then subplebbitAddress for backward compatibility) const boardIdentifier = params.boardIdentifier || params.subplebbitAddress; // 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 ( ); }; export const UpdateButton = () => { const { t } = useTranslation(); const reset = useFeedResetStore((state) => state.reset); return ( ); }; export const AutoButton = () => { const { t } = useTranslation(); const isMobile = useIsMobile(); const handleAutoClick = () => { window.alert(t('posts_auto_update_info')); }; return ( <> {isMobile ? ( ) : ( )} ); }; const BottomButton = () => { const { t } = useTranslation(); const handleClick = () => { window.scrollTo({ top: document.documentElement.scrollHeight, behavior: 'instant' }); }; return ( ); }; export const TopButton = () => { const { t } = useTranslation(); const handleClick = () => { 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 AllFeedFilter = () => { const { t } = useTranslation(); const { filter, setFilter } = useAllFeedFilterStore(); return ( <> {t('show')} ); }; 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 = useAccountComment({ commentIndex: params?.accountCommentIndex as any }); const resolvedAddress = useResolvedSubplebbitAddress(); const subplebbitAddress = resolvedAddress || accountComment?.subplebbitAddress; const { filteredCount, searchText } = useCatalogFiltersStore(); const enableInfiniteScroll = useFeedViewSettingsStore((state) => state.enableInfiniteScroll); const isMultiboard = isInAllView || isInSubscriptionsView || isInModView; 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 || params.subplebbitAddress; const showVoteButton = boardIdentifier && isDirectoryBoard(boardIdentifier, directories); return (
{isInPostView || isInPendingPostPage ? ( <> {showBottomButton && }
) : isInModQueueView ? ( <> ) : ( <> {isInCatalogView ? ( ) : ( )} {showVoteButton && } {!(isInAllView || isInSubscriptionsView || isInModView) && } {!(isInAllView || isInSubscriptionsView) && } {showBottomButton && } {isInCatalogView && searchText ? ( {' '} — {t('search_results_for')}: {searchText} ) : ( isInCatalogView && filteredCount > 0 && ( {' '} — {t('filtered_threads')}: {filteredCount} ) )} {isInAllView && ( <>
)} {isInCatalogView && ( <>
)} )}
); }; export const PostPageStats = () => { const { t } = useTranslation(); const params = useParams(); const location = useLocation(); const commentCid = params?.commentCid as string | undefined; const resolvedAddress = useResolvedSubplebbitAddress(); const accountComment = useAccountComment({ commentIndex: params?.accountCommentIndex as any }); const subplebbitAddress = resolvedAddress || accountComment?.subplebbitAddress; const comment = useComment({ commentCid }); const postCid = comment?.postCid ?? commentCid; const post = useComment({ commentCid: postCid }); const { closed, pinned, replyCount } = post || {}; const linkCount = useCountLinksInReplies(post); const directoryEntry = useDirectoryByAddress(subplebbitAddress); const requirePostLinkIsMedia = directoryEntry?.features?.requirePostLinkIsMedia === true; const isThreadView = isPostPageView(location.pathname, params); const pageNumber = usePostPageNumber({ subplebbitAddress, postCid, enabled: isThreadView, }); const displayReplyCount = replyCount !== undefined ? replyCount.toString() : '?'; const replyCountTooltip = replyCount !== undefined ? capitalize(t('replies')) : t('loading'); return ( {pinned && `${capitalize(t('sticky'))} / `} {closed && `${capitalize(t('closed'))} / `} {displayReplyCount} /{' '} {linkCount?.toString()} {isThreadView && ( <> {' '} / {pageNumber?.toString() ?? '?'} )} ); }; export const DesktopBoardButtons = () => { const { t } = useTranslation(); const params = useParams(); const location = useLocation(); const accountComment = useAccountComment({ commentIndex: params?.accountCommentIndex as any }); const resolvedAddress = useResolvedSubplebbitAddress(); const subplebbitAddress = resolvedAddress || accountComment?.subplebbitAddress; 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 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 || params.subplebbitAddress; const showVoteButton = boardIdentifier && isDirectoryBoard(boardIdentifier, directories); return ( <>
{isInPostView || isInPendingPostPage ? ( <> [] [ ] {showBottomButton && ( <> {' '} [] )}{' '} [] [] ) : isInModQueueView ? ( <> [ ] [] ) : ( <> {isInCatalogView ? ( <> []{' '} ) : ( <> []{' '} )} {showVoteButton && ( <> {' '} [] )} {showBottomButton && ( <> {' '} [] )}{' '} [] {!(isInAllView || isInSubscriptionsView) && ( <> {' '} )} {isInCatalogView && searchText ? ( {' '} — {t('search_results_for')}: {searchText} ) : ( isInCatalogView && filteredCount > 0 && ( {' '} — {t('filtered_threads')}: {filteredCount} ) )} {isInCatalogView && ( <> )} {isInAllView && } {!(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 = useResolvedSubplebbitAddress(); const boardPath = resolvedAddress ? getBoardPath(resolvedAddress, directories) : params?.boardIdentifier || params?.subplebbitAddress; const handleSearch = (event: React.KeyboardEvent) => { if (event.key === 'Enter') { const searchQuery = (event.target as HTMLInputElement).value.trim(); if (searchQuery) { let catalogUrl = ''; if (isInAllView) { catalogUrl = `/all/catalog?q=${encodeURIComponent(searchQuery)}`; } else if (isInSubscriptionsView) { catalogUrl = `/subs/catalog?q=${encodeURIComponent(searchQuery)}`; } else if (isInModView) { catalogUrl = `/mod/catalog?q=${encodeURIComponent(searchQuery)}`; } else { catalogUrl = `/${boardPath}/catalog?q=${encodeURIComponent(searchQuery)}`; } navigate(catalogUrl); } } }; return ; };