mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
fix(react): address Doctor findings (#1143)
Accessibility/semantics pass aligned with React Doctor: role-based spans/divs become real buttons, role=status becomes <output>, modals use native <dialog>, author/peer flags render as <img>, and embed/challenge iframes are sandboxed. Includes review follow-ups: correct button chrome/centering/themed dialog colors and focus rings, the missing aria-label i18n keys (all languages), the Yandex image-search URL, clearer .bso setup steps, a keyboard-accessible image-search dropdown, and removal of the post-edit ('comment edited') display (mod edits unchanged).
This commit is contained in:
@@ -23,14 +23,13 @@ type EditorState = {
|
||||
};
|
||||
|
||||
const loadAce = async () => {
|
||||
const aceModule = await import('react-ace');
|
||||
const [workerJsonModule] = await Promise.all([
|
||||
import('ace-builds/src-noconflict/worker-json?url'),
|
||||
import('ace-builds/esm-resolver'),
|
||||
import('ace-builds/src-noconflict/mode-json'),
|
||||
import('ace-builds/src-noconflict/theme-monokai'),
|
||||
]);
|
||||
// Load react-ace first so esm-resolver sees the global ace instance.
|
||||
const aceModulePromise = import('react-ace');
|
||||
const workerJsonModulePromise = import('ace-builds/src-noconflict/worker-json?url');
|
||||
const modeJsonPromise = import('ace-builds/src-noconflict/mode-json');
|
||||
const themeMonokaiPromise = import('ace-builds/src-noconflict/theme-monokai');
|
||||
const resolverPromise = aceModulePromise.then(() => import('ace-builds/esm-resolver'));
|
||||
const [aceModule, workerJsonModule] = await Promise.all([aceModulePromise, workerJsonModulePromise, resolverPromise, modeJsonPromise, themeMonokaiPromise]);
|
||||
// esm-resolver waits for react-ace so it can see the global ace instance.
|
||||
const mod = aceModule.default;
|
||||
const Editor = typeof mod === 'function' ? mod : (mod as unknown as { default: typeof mod }).default;
|
||||
|
||||
@@ -143,6 +142,7 @@ const AccountDataEditor = () => {
|
||||
/>
|
||||
) : (
|
||||
<textarea
|
||||
aria-label={t('account_data')}
|
||||
value={text}
|
||||
onChange={(e) => setEditorState((current) => ({ ...current, text: e.target.value }))}
|
||||
style={{ width: '100%', height: '500px', fontFamily: 'monospace', fontSize: 13 }}
|
||||
|
||||
@@ -241,7 +241,7 @@ const Archive = () => {
|
||||
<tr>
|
||||
<td className={styles.postblock}>No.</td>
|
||||
<td className={styles.postblock}>Excerpt</td>
|
||||
<td className={styles.postblock}></td>
|
||||
<td className={styles.postblock} aria-label='Actions'></td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import blotterData from '../../data/5chan-blotter.json';
|
||||
import BlotterMessage from '../../components/blotter-message';
|
||||
import { formatBlotterDate, isBlotterEntry, sortBlotterEntries } from '../../lib/utils/blotter-utils';
|
||||
import { Footer } from '../home';
|
||||
import { Footer } from '../home/home';
|
||||
import styles from './blotter.module.css';
|
||||
|
||||
const Blotter = () => {
|
||||
|
||||
@@ -646,7 +646,7 @@ describe('Board', () => {
|
||||
|
||||
expect(latestLocation).toBe('/subs');
|
||||
expect(container.textContent).toContain('not_subscribed_to_any_board');
|
||||
expect(container.querySelector('[role="status"]')?.className).toContain('searchNothingFound');
|
||||
expect(container.querySelector('output')?.className).toContain('searchNothingFound');
|
||||
});
|
||||
|
||||
it('links empty mod feeds to account import settings', async () => {
|
||||
@@ -660,7 +660,7 @@ describe('Board', () => {
|
||||
|
||||
expect(container.textContent).toContain('not_mod_of_any_board');
|
||||
expect(container.textContent).toContain('go_to_settings_to_import_mod_account');
|
||||
expect(container.querySelector('[role="status"]')?.className).toContain('modEmptyState');
|
||||
expect(container.querySelector('output')?.className).toContain('modEmptyState');
|
||||
expect(container.querySelector('a')?.getAttribute('href')).toBe('/mod/settings#account-settings');
|
||||
});
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
}
|
||||
|
||||
.addressWarning {
|
||||
display: block;
|
||||
color: var(--mod-queue-alert-color, red);
|
||||
padding: 15px 5px 0 5px;
|
||||
}
|
||||
|
||||
+25
-29
@@ -109,6 +109,7 @@ const BoardFooter = ({
|
||||
1: onExpandTimeWindow ? (
|
||||
<button
|
||||
type='button'
|
||||
aria-label={t('load_more')}
|
||||
data-testid='expand-time-window-button'
|
||||
className={styles.morePostsSuggestionAction}
|
||||
onClick={() => {
|
||||
@@ -137,9 +138,7 @@ const BoardFooter = ({
|
||||
{communityState === 'failed' ? (
|
||||
<span className='red'>{communityState}</span>
|
||||
) : isInSubscriptionsView && subscriptionsLength === 0 ? (
|
||||
<div className={styles.searchNothingFound} role='status'>
|
||||
{t('not_subscribed_to_any_board')}
|
||||
</div>
|
||||
<output className={styles.searchNothingFound}>{t('not_subscribed_to_any_board')}</output>
|
||||
) : isInModView && accountCommunityAddressesLength === 0 ? (
|
||||
<ModEmptyState />
|
||||
) : (
|
||||
@@ -160,7 +159,7 @@ export interface BoardProps {
|
||||
|
||||
const Board = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp, timeFilterNameFromCache, isVisible = true }: BoardProps) => {
|
||||
const { t } = useTranslation();
|
||||
const location = useLocation();
|
||||
const routerLocation = useLocation();
|
||||
const params = useParams();
|
||||
const isInAllView = viewType ? viewType === 'all' : false;
|
||||
const isInSubscriptionsView = viewType ? viewType === 'subs' : false;
|
||||
@@ -233,7 +232,7 @@ const Board = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp, t
|
||||
const shouldProbeYearlyFeed = shouldProbeSuggestionFeeds && currentTimeFilterSeconds < YEAR_IN_SECONDS;
|
||||
// Keep suggestion feeds on a stable hook identity; the loader widens them by paging, not by recreating the feed.
|
||||
const suggestionPostsPerPage = infiniteFeedPostsPerPage;
|
||||
const suggestionRequestKeyBase = `${location.pathname}${location.search}`;
|
||||
const suggestionRequestKeyBase = `${routerLocation.pathname}${routerLocation.search}`;
|
||||
const {
|
||||
feed: weeklyFeed,
|
||||
hasMore: weeklyFeedHasMore,
|
||||
@@ -303,7 +302,7 @@ const Board = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp, t
|
||||
[communityAddress],
|
||||
);
|
||||
const { accountComments: recentAccountComments } = useAccountComments(accountCommentLookupOptions);
|
||||
const nonokoPendingAccountCommentIndex = getNonokoPendingAccountCommentIndex(location.state);
|
||||
const nonokoPendingAccountCommentIndex = getNonokoPendingAccountCommentIndex(routerLocation.state);
|
||||
const nonokoPendingAccountCommentLookupOptions = useMemo(
|
||||
() =>
|
||||
typeof nonokoPendingAccountCommentIndex === 'number'
|
||||
@@ -315,7 +314,7 @@ const Board = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp, t
|
||||
);
|
||||
const { accountComments: nonokoPendingAccountComments } = useAccountComments(nonokoPendingAccountCommentLookupOptions);
|
||||
|
||||
const pathWithoutSettings = location.pathname.replace(/\/settings$/, '');
|
||||
const pathWithoutSettings = routerLocation.pathname.replace(/\/settings$/, '');
|
||||
const currentPage = getPageFromFeedPath(pathWithoutSettings);
|
||||
const paginationBasePath = stripPageFromFeedPath(pathWithoutSettings);
|
||||
|
||||
@@ -396,8 +395,8 @@ const Board = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp, t
|
||||
const navigate = useNavigate();
|
||||
const defaultFeedVirtualizationMode = isMobile && isMultiboardView ? 'off' : 'item-size';
|
||||
const feedVirtualizationMode = useMemo(
|
||||
() => resolveFeedVirtualizationMode(location.search, defaultFeedVirtualizationMode),
|
||||
[defaultFeedVirtualizationMode, location.search],
|
||||
() => resolveFeedVirtualizationMode(routerLocation.search, defaultFeedVirtualizationMode),
|
||||
[defaultFeedVirtualizationMode, routerLocation.search],
|
||||
);
|
||||
const defaultBoardItemHeight = feedVirtualizationMode === 'item-size' ? (isMobile ? 420 : 480) : isMobile ? 420 : 300;
|
||||
// Omit the prop entirely in fallback mode. Passing `itemSize={undefined}` overrides
|
||||
@@ -407,20 +406,20 @@ const Board = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp, t
|
||||
// Redirect multiboard paths with page-number segments to normalized path (infinite-scroll only)
|
||||
useEffect(() => {
|
||||
if (!isVisible || !isForcedInfiniteScroll) return;
|
||||
const normalized = normalizeMultiboardFeedPath(location.pathname);
|
||||
if (normalized !== location.pathname) {
|
||||
navigate({ pathname: normalized, search: location.search }, { replace: true });
|
||||
const normalized = normalizeMultiboardFeedPath(routerLocation.pathname);
|
||||
if (normalized !== routerLocation.pathname) {
|
||||
navigate({ pathname: normalized, search: routerLocation.search }, { replace: true });
|
||||
}
|
||||
}, [isVisible, isForcedInfiniteScroll, location.pathname, location.search, navigate]);
|
||||
}, [isVisible, isForcedInfiniteScroll, routerLocation.pathname, routerLocation.search, navigate]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isVisible) return;
|
||||
if (!effectiveInfiniteScroll && currentPage > totalPages && totalPages > 0) {
|
||||
const targetPage = totalPages;
|
||||
const targetPath = targetPage === 1 ? paginationBasePath : `${paginationBasePath}/${targetPage}`;
|
||||
navigate({ pathname: targetPath, search: location.search }, { replace: true });
|
||||
navigate({ pathname: targetPath, search: routerLocation.search }, { replace: true });
|
||||
}
|
||||
}, [isVisible, effectiveInfiniteScroll, currentPage, totalPages, paginationBasePath, location.search, navigate]);
|
||||
}, [isVisible, effectiveInfiniteScroll, currentPage, totalPages, paginationBasePath, routerLocation.search, navigate]);
|
||||
|
||||
// Scroll to top instantly when page changes in pagination mode
|
||||
useEffect(() => {
|
||||
@@ -467,7 +466,7 @@ const Board = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp, t
|
||||
currentTimeFilterName={currentTimeFilterName}
|
||||
moreThreadsSuggestion={moreThreadsSuggestion}
|
||||
moreThreadsSuggestionPathname={moreThreadsSuggestionPathname}
|
||||
moreThreadsSuggestionSearch={location.search}
|
||||
moreThreadsSuggestionSearch={routerLocation.search}
|
||||
onExpandTimeWindow={expandSuggestionTimeWindow}
|
||||
communityState={communityState}
|
||||
subscriptionsLength={subscriptions?.length || 0}
|
||||
@@ -479,7 +478,7 @@ const Board = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp, t
|
||||
<BoardPagination
|
||||
basePath={paginationBasePath}
|
||||
currentPage={currentPage}
|
||||
search={location.search}
|
||||
search={routerLocation.search}
|
||||
totalPages={totalPages}
|
||||
footerStyle
|
||||
isMultiboard={isForcedInfiniteScroll}
|
||||
@@ -490,16 +489,16 @@ const Board = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp, t
|
||||
<div>
|
||||
{!isForcedInfiniteScroll && (
|
||||
<div className={mobileFooterStyles.mobileFooterButtons}>
|
||||
<button className='button' onClick={() => window.scrollTo({ top: 0, left: 0, behavior: 'instant' })}>
|
||||
<button type='button' className='button' onClick={() => window.scrollTo({ top: 0, left: 0, behavior: 'instant' })}>
|
||||
{t('start_new_thread')}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<div className={mobileFooterStyles.mobileFooterButtons}>
|
||||
<button className='button' onClick={() => window.scrollTo({ top: 0, left: 0, behavior: 'instant' })}>
|
||||
<button type='button' className='button' onClick={() => window.scrollTo({ top: 0, left: 0, behavior: 'instant' })}>
|
||||
{t('top')}
|
||||
</button>
|
||||
<button className='button' onClick={() => reset && reset()}>
|
||||
<button type='button' className='button' onClick={() => reset && reset()}>
|
||||
{t('refresh')}
|
||||
</button>
|
||||
</div>
|
||||
@@ -511,7 +510,7 @@ const Board = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp, t
|
||||
<span key={page}>
|
||||
[
|
||||
<Link
|
||||
to={{ pathname: page === 1 ? paginationBasePath : `${paginationBasePath}/${page}`, search: location.search }}
|
||||
to={{ pathname: page === 1 ? paginationBasePath : `${paginationBasePath}/${page}`, search: routerLocation.search }}
|
||||
className={page === currentPage ? mobileFooterStyles.mobileFooterPaginationCurrent : undefined}
|
||||
>
|
||||
{page}
|
||||
@@ -527,7 +526,7 @@ const Board = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp, t
|
||||
)}
|
||||
{hasMore && !effectiveInfiniteScroll && (
|
||||
<div className={mobileFooterStyles.mobileFooterButtons}>
|
||||
<button className='button' onClick={() => setEnableInfiniteScroll(true)}>
|
||||
<button type='button' className='button' onClick={() => setEnableInfiniteScroll(true)}>
|
||||
{t('load_more')}
|
||||
</button>
|
||||
</div>
|
||||
@@ -541,6 +540,7 @@ const Board = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp, t
|
||||
communityAddresses,
|
||||
hasMore,
|
||||
combinedFeed.length,
|
||||
isInAllView,
|
||||
isInSubscriptionsView,
|
||||
isInModView,
|
||||
currentTimeFilterName,
|
||||
@@ -559,13 +559,13 @@ const Board = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp, t
|
||||
totalPages,
|
||||
setEnableInfiniteScroll,
|
||||
reset,
|
||||
location.search,
|
||||
routerLocation.search,
|
||||
t,
|
||||
],
|
||||
);
|
||||
|
||||
const virtuosoRef = useRef<VirtuosoHandle | null>(null);
|
||||
const virtuosoStateKey = feedCacheKey ? `${feedCacheKey}-${BOARD_SORT_TYPE}` : `${location.pathname}${location.search}-${BOARD_SORT_TYPE}`;
|
||||
const virtuosoStateKey = feedCacheKey ? `${feedCacheKey}-${BOARD_SORT_TYPE}` : `${routerLocation.pathname}${routerLocation.search}-${BOARD_SORT_TYPE}`;
|
||||
const navigationType = useNavigationType();
|
||||
const boardViewportBuffer = isMultiboardView ? (isMobile ? { bottom: 1400, top: 2400 } : { bottom: 1200, top: 2400 }) : { bottom: 1200, top: 1200 };
|
||||
const boardMinOverscanItemCount = isMultiboardView && isMobile ? { bottom: 4, top: 8 } : undefined;
|
||||
@@ -645,11 +645,7 @@ const Board = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp, t
|
||||
<ErrorDisplay error={communityError} />
|
||||
</div>
|
||||
)}
|
||||
{shouldShowUnverifiedAddressWarning && (
|
||||
<div className={styles.addressWarning} role='status'>
|
||||
{t('board_address_unverified_warning')}
|
||||
</div>
|
||||
)}
|
||||
{shouldShowUnverifiedAddressWarning && <output className={styles.addressWarning}>{t('board_address_unverified_warning')}</output>}
|
||||
{effectiveInfiniteScroll ? (
|
||||
<Virtuoso
|
||||
defaultItemHeight={defaultBoardItemHeight}
|
||||
|
||||
@@ -3,7 +3,8 @@ import { createElement } from 'react';
|
||||
import { createRoot, type Root } from 'react-dom/client';
|
||||
import { MemoryRouter, Route, Routes, useLocation } from 'react-router-dom';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import Catalog, { getCatalogRenderFeed, type CatalogProps } from '../catalog';
|
||||
import Catalog, { type CatalogProps } from '../catalog';
|
||||
import { getCatalogRenderFeed } from '../catalog-render-feed';
|
||||
import { clearStableLastVisitTimeFilterName, LAST_VISIT_STORAGE_KEY } from '../../../lib/utils/time-filter-utils';
|
||||
import useHiddenCatalogThreadsStore from '../../../stores/use-hidden-catalog-threads-store';
|
||||
|
||||
@@ -920,7 +921,7 @@ describe('Catalog', () => {
|
||||
|
||||
expect(container.textContent).toContain('not_mod_of_any_board');
|
||||
expect(container.textContent).toContain('go_to_settings_to_import_mod_account');
|
||||
expect(container.querySelector('[role="status"]')?.className).toContain('modEmptyState');
|
||||
expect(container.querySelector('output')?.className).toContain('modEmptyState');
|
||||
expect(container.querySelector('a')?.getAttribute('href')).toBe('/mod/settings#account-settings');
|
||||
expect(container.querySelectorAll('[data-testid="catalog-row"]')).toHaveLength(0);
|
||||
});
|
||||
@@ -934,8 +935,8 @@ describe('Catalog', () => {
|
||||
|
||||
expect(container.textContent).toContain('nothing_found');
|
||||
expect(container.textContent).not.toContain('no_threads');
|
||||
expect(container.querySelector('[role="status"]')?.className).toContain('searchNothingFound');
|
||||
expect(container.querySelector('[role="status"]')?.textContent).toBe('nothing_found');
|
||||
expect(container.querySelector('output')?.className).toContain('searchNothingFound');
|
||||
expect(container.querySelector('output')?.textContent).toBe('nothing_found');
|
||||
expect(container.querySelectorAll('[data-testid="catalog-row"]')).toHaveLength(0);
|
||||
expect(container.textContent).toContain('search_results_for');
|
||||
expect(container.textContent).toContain('asddasd');
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
export const getCatalogRenderFeed = <T>(processedFeed: readonly T[], deferredProcessedFeed: readonly T[]): readonly T[] =>
|
||||
deferredProcessedFeed.length === 0 && processedFeed.length > 0 ? processedFeed : deferredProcessedFeed;
|
||||
@@ -44,6 +44,7 @@ import {
|
||||
readReplyTypographyMetrics,
|
||||
resolveCatalogVirtualizationMode,
|
||||
} from '../../lib/utils/pretext-height-estimates';
|
||||
import { getCatalogRenderFeed } from './catalog-render-feed';
|
||||
|
||||
const lastVirtuosoStates: { [key: string]: StateSnapshot } = {};
|
||||
const RECENT_ACCOUNT_COMMENT_WINDOW_SECONDS = 60 * 60;
|
||||
@@ -52,8 +53,6 @@ const MONTH_IN_SECONDS = 30 * 24 * 60 * 60;
|
||||
const YEAR_IN_SECONDS = 365 * 24 * 60 * 60;
|
||||
// Keep the hook on its indexed fast path when this view should not inject local posts.
|
||||
const EMPTY_ACCOUNT_COMMENT_LOOKUP = { commentIndices: [-1] };
|
||||
export const getCatalogRenderFeed = <T,>(processedFeed: readonly T[], deferredProcessedFeed: readonly T[]): readonly T[] =>
|
||||
deferredProcessedFeed.length === 0 && processedFeed.length > 0 ? processedFeed : deferredProcessedFeed;
|
||||
|
||||
interface CatalogFooterProps {
|
||||
communityAddresses: string[];
|
||||
@@ -97,6 +96,7 @@ const CatalogFooter = ({
|
||||
1: onExpandTimeWindow ? (
|
||||
<button
|
||||
type='button'
|
||||
aria-label={t('load_more')}
|
||||
data-testid='expand-time-window-button'
|
||||
className={styles.morePostsSuggestionAction}
|
||||
onClick={() => {
|
||||
@@ -139,11 +139,7 @@ interface CatalogSearchNothingFoundProps {
|
||||
const CatalogSearchNothingFound = ({ className }: CatalogSearchNothingFoundProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className={className ?? styles.searchNothingFound} role='status'>
|
||||
{t('nothing_found')}
|
||||
</div>
|
||||
);
|
||||
return <output className={className ?? styles.searchNothingFound}>{t('nothing_found')}</output>;
|
||||
};
|
||||
|
||||
interface CatalogLoadingProps {
|
||||
@@ -188,6 +184,7 @@ const createContentFilter = (
|
||||
) => {
|
||||
// Create a unique key based on the enabled filter items
|
||||
const enabledFilters = filterItems.filter((item) => item.enabled && item.text.trim() !== '');
|
||||
const filterIndexByItem = new Map(filterItems.map((item, index) => [item, index]));
|
||||
const filterKey =
|
||||
enabledFilters.length > 0
|
||||
? `content-filter-${enabledFilters.map((item) => `${item.text}-${item.hide ? 'hide' : ''}-${item.top ? 'top' : ''}`).join('-')}`
|
||||
@@ -206,7 +203,7 @@ const createContentFilter = (
|
||||
|
||||
if (commentMatchesPattern(comment, pattern)) {
|
||||
// Find the original filter index to increment count
|
||||
const filterIndex = filterItems.findIndex((f) => f.text === item.text && f.enabled);
|
||||
const filterIndex = filterIndexByItem.get(item) ?? -1;
|
||||
if (trackMatches && filterIndex !== -1) {
|
||||
if (onFilterMatch) {
|
||||
onFilterMatch(filterIndex, comment.cid, communityAddress);
|
||||
@@ -271,7 +268,7 @@ export interface CatalogProps {
|
||||
|
||||
const Catalog = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp, timeFilterNameFromCache, isVisible = true }: CatalogProps) => {
|
||||
const { t } = useTranslation();
|
||||
const location = useLocation();
|
||||
const routerLocation = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const params = useParams();
|
||||
|
||||
@@ -338,15 +335,15 @@ const Catalog = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp,
|
||||
// Canonical redirect for multiboard catalog paths with numeric page segment (e.g. /all/catalog/1w/5 -> /all/catalog/1w)
|
||||
useEffect(() => {
|
||||
if (!(isInAllView || isInSubscriptionsView || isInModView)) return;
|
||||
const canonical = normalizeMultiboardFeedPath(location.pathname);
|
||||
if (location.pathname !== canonical) {
|
||||
navigate({ pathname: canonical, search: location.search }, { replace: true });
|
||||
const canonical = normalizeMultiboardFeedPath(routerLocation.pathname);
|
||||
if (routerLocation.pathname !== canonical) {
|
||||
navigate({ pathname: canonical, search: routerLocation.search }, { replace: true });
|
||||
}
|
||||
}, [isInAllView, isInSubscriptionsView, isInModView, location.pathname, location.search, navigate]);
|
||||
}, [isInAllView, isInSubscriptionsView, isInModView, routerLocation.pathname, routerLocation.search, navigate]);
|
||||
|
||||
const sortType = useSortingStore((state) => state.sortType);
|
||||
const feedSortType = sortType === 'new' ? 'new' : 'active';
|
||||
const catalogVirtualizationMode = useMemo(() => resolveCatalogVirtualizationMode(location.search, 'item-size'), [location.search]);
|
||||
const catalogVirtualizationMode = useMemo(() => resolveCatalogVirtualizationMode(routerLocation.search, 'item-size'), [routerLocation.search]);
|
||||
const themeKey = typeof document !== 'undefined' ? document.body.className : '';
|
||||
const hadVisibleHiddenThreadsRef = useRef(false);
|
||||
|
||||
@@ -441,7 +438,7 @@ const Catalog = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp,
|
||||
);
|
||||
// Keep suggestion feeds on a stable hook identity; the loader widens them by paging, not by recreating the feed.
|
||||
const suggestionPostsPerPage = multiboardCatalogPostsPerPage;
|
||||
const suggestionRequestKeyBase = `${location.pathname}${location.search}`;
|
||||
const suggestionRequestKeyBase = `${routerLocation.pathname}${routerLocation.search}`;
|
||||
const {
|
||||
feed: weeklyFeed,
|
||||
hasMore: weeklyFeedHasMore,
|
||||
@@ -599,10 +596,11 @@ const Catalog = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp,
|
||||
const footerMoreThreadsSuggestion = showHiddenThreads ? null : moreThreadsSuggestion;
|
||||
const footerShowLoadingEllipsis = showHiddenThreads ? isLoadingHiddenCatalogThreads : effectiveInfiniteScroll;
|
||||
|
||||
const catalogFooterFirstRow = (
|
||||
<CatalogFooterFirstRow communityAddress={communityAddress} isInAllView={isInAllView} isInSubscriptionsView={isInSubscriptionsView} isInModView={isInModView} />
|
||||
const catalogFooterFirstRow = useMemo(
|
||||
() => <CatalogFooterFirstRow communityAddress={communityAddress} isInAllView={isInAllView} isInSubscriptionsView={isInSubscriptionsView} isInModView={isInModView} />,
|
||||
[communityAddress, isInAllView, isInSubscriptionsView, isInModView],
|
||||
);
|
||||
const catalogFooterStyleRow = <CatalogFooterStyleRow />;
|
||||
const catalogFooterStyleRow = useMemo(() => <CatalogFooterStyleRow />, []);
|
||||
|
||||
// Memoize footer component to preserve identity across renders (Virtuoso optimization)
|
||||
// Note: useFeedStateString is called inside CatalogFooter to isolate re-renders from backend state changes
|
||||
@@ -617,7 +615,7 @@ const Catalog = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp,
|
||||
currentTimeFilterName={currentTimeFilterName}
|
||||
moreThreadsSuggestion={footerMoreThreadsSuggestion}
|
||||
moreThreadsSuggestionPathname={moreThreadsSuggestionPathname}
|
||||
moreThreadsSuggestionSearch={location.search}
|
||||
moreThreadsSuggestionSearch={routerLocation.search}
|
||||
onExpandTimeWindow={expandSuggestionTimeWindow}
|
||||
showLoadingEllipsis={footerShowLoadingEllipsis}
|
||||
/>
|
||||
@@ -647,7 +645,7 @@ const Catalog = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp,
|
||||
isInAllView,
|
||||
isInSubscriptionsView,
|
||||
isInModView,
|
||||
location.search,
|
||||
routerLocation.search,
|
||||
footerShowLoadingEllipsis,
|
||||
],
|
||||
);
|
||||
@@ -661,7 +659,7 @@ const Catalog = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp,
|
||||
currentTimeFilterName={currentTimeFilterName}
|
||||
moreThreadsSuggestion={footerMoreThreadsSuggestion}
|
||||
moreThreadsSuggestionPathname={moreThreadsSuggestionPathname}
|
||||
moreThreadsSuggestionSearch={location.search}
|
||||
moreThreadsSuggestionSearch={routerLocation.search}
|
||||
onExpandTimeWindow={expandSuggestionTimeWindow}
|
||||
showLoadingEllipsis={footerShowLoadingEllipsis}
|
||||
/>
|
||||
@@ -690,7 +688,7 @@ const Catalog = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp,
|
||||
isInAllView,
|
||||
isInSubscriptionsView,
|
||||
isInModView,
|
||||
location.search,
|
||||
routerLocation.search,
|
||||
footerShowLoadingEllipsis,
|
||||
],
|
||||
);
|
||||
@@ -757,7 +755,13 @@ const Catalog = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp,
|
||||
continue;
|
||||
}
|
||||
|
||||
const firstMatch = activeColoredFilters.find((item) => commentMatchesPattern(comment, item.text));
|
||||
let firstMatch: (typeof activeColoredFilters)[number] | undefined;
|
||||
for (const filter of activeColoredFilters) {
|
||||
if (commentMatchesPattern(comment, filter.text)) {
|
||||
firstMatch = filter;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (firstMatch?.color) {
|
||||
nextMatchedFilterColors.set(cid, firstMatch.color);
|
||||
}
|
||||
@@ -788,7 +792,11 @@ const Catalog = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp,
|
||||
return nextRows;
|
||||
}, [catalogRenderFeed, columnCount, isFeedLoaded]);
|
||||
|
||||
const catalogMetrics = useMemo(() => readReplyTypographyMetrics(), [themeKey, windowWidth]);
|
||||
const catalogMetrics = useMemo(() => {
|
||||
void themeKey;
|
||||
void windowWidth;
|
||||
return readReplyTypographyMetrics();
|
||||
}, [themeKey, windowWidth]);
|
||||
const rowHeightEstimates = useMemo(
|
||||
() =>
|
||||
catalogVirtualizationMode === 'off'
|
||||
@@ -812,7 +820,7 @@ const Catalog = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp,
|
||||
const virtuosoRef = useRef<VirtuosoHandle | null>(null);
|
||||
const virtuosoStateKey = feedCacheKey
|
||||
? `${feedCacheKey}-${sortType}-${processedFeedMode}`
|
||||
: `${location.pathname}${location.search}-${sortType}-${processedFeedMode}-catalog`;
|
||||
: `${routerLocation.pathname}${routerLocation.search}-${sortType}-${processedFeedMode}-catalog`;
|
||||
const navigationType = useNavigationType();
|
||||
|
||||
const hasBeenVisibleRef = useRef(false);
|
||||
|
||||
@@ -288,6 +288,6 @@ describe('Directory', () => {
|
||||
|
||||
const cells = Array.from(getDirectoryRow('board-6.bso')?.querySelectorAll('td') ?? []).map((cell) => cell.textContent?.replace(/\s+/g, ' ').trim());
|
||||
expect(cells[3]).toBe('—?');
|
||||
expect(getDirectoryRow('board-6.bso')?.querySelector('sup')?.closest('span')?.getAttribute('title')).toBe('directory_status_unavailable_reason');
|
||||
expect(getDirectoryRow('board-6.bso')?.querySelector('button[aria-label="directory_status_unavailable_reason"]')).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -196,11 +196,20 @@
|
||||
}
|
||||
|
||||
.statusUnavailableHelp {
|
||||
all: unset;
|
||||
margin-left: 2px;
|
||||
font-weight: 700;
|
||||
font-size: 0.75em;
|
||||
vertical-align: super;
|
||||
line-height: 0;
|
||||
cursor: help;
|
||||
}
|
||||
|
||||
.statusUnavailableHelp:focus-visible {
|
||||
outline: 1px dotted currentcolor;
|
||||
outline-offset: 1px;
|
||||
}
|
||||
|
||||
.directoryFootnote {
|
||||
width: 80%;
|
||||
max-width: 810px;
|
||||
|
||||
@@ -129,9 +129,9 @@ const DirectoryRow = ({ board, nowSeconds, rank, onVote }: DirectoryRowProps) =>
|
||||
<span className={styles.statusUnavailable}>
|
||||
{DIRECTORY_STATUS_UNAVAILABLE_MARKER}
|
||||
<Tooltip content={statusUnavailableReason}>
|
||||
<sup className={styles.statusUnavailableHelp} aria-label={statusUnavailableReason} tabIndex={0}>
|
||||
<button type='button' className={styles.statusUnavailableHelp} aria-label={statusUnavailableReason} tabIndex={0}>
|
||||
?
|
||||
</sup>
|
||||
</button>
|
||||
</Tooltip>
|
||||
</span>
|
||||
) : status === 'loading' ? (
|
||||
@@ -252,7 +252,7 @@ const Directory = () => {
|
||||
values={{ boardIdentifier }}
|
||||
components={{
|
||||
passLink: <Link to={PASS_LINK} />,
|
||||
repoLink: <a href={repoEditUrl} target='_blank' rel='noreferrer noopener' />,
|
||||
repoLink: <a href={repoEditUrl} target='_blank' rel='noreferrer noopener' aria-label={t('directory_submit_repo_link_label', 'directory repository')} />,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { type ReactNode, useEffect } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { HashLink } from 'react-router-hash-link';
|
||||
import { Footer, HomeLogo } from '../home';
|
||||
import { Footer, HomeLogo } from '../home/home';
|
||||
import styles from './faq.module.css';
|
||||
|
||||
const externalLinkProps = {
|
||||
|
||||
@@ -8,7 +8,7 @@ const BoardsFilterModal = () => {
|
||||
const { t } = useTranslation();
|
||||
const [showFilterModal, setShowFilterModal] = useState(false);
|
||||
const modalRef = useRef<HTMLDivElement>(null);
|
||||
const buttonRef = useRef<HTMLSpanElement>(null);
|
||||
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||
|
||||
const { useCatalogLinks, setUseCatalogLinks, boardFilter, setBoardFilter } = useBoardsFilterStore();
|
||||
|
||||
@@ -38,9 +38,9 @@ const BoardsFilterModal = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<span
|
||||
<button
|
||||
type='button'
|
||||
ref={buttonRef}
|
||||
role='button'
|
||||
tabIndex={0}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
@@ -51,13 +51,13 @@ const BoardsFilterModal = () => {
|
||||
onClick={() => !showFilterModal && setShowFilterModal(true)}
|
||||
>
|
||||
{t('filter')} ▼
|
||||
</span>
|
||||
</button>
|
||||
{showFilterModal && (
|
||||
<div ref={modalRef} className={styles.filterModal}>
|
||||
{/* Always shown: Use Catalog */}
|
||||
<div
|
||||
<button
|
||||
type='button'
|
||||
className={`${styles.option} ${useCatalogLinks && styles.selected}`}
|
||||
role='button'
|
||||
tabIndex={0}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
@@ -72,15 +72,15 @@ const BoardsFilterModal = () => {
|
||||
}}
|
||||
>
|
||||
{t('use_catalog')}
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{/* Conditionally shown: Filtering options (only after disclaimer accepted) */}
|
||||
{disclaimerAccepted && (
|
||||
<>
|
||||
<div className={styles.separator} />
|
||||
<div
|
||||
<button
|
||||
type='button'
|
||||
className={`${styles.option} ${boardFilter === 'all' && styles.selected}`}
|
||||
role='button'
|
||||
tabIndex={0}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
@@ -95,10 +95,10 @@ const BoardsFilterModal = () => {
|
||||
}}
|
||||
>
|
||||
{t('show_all_boards')}
|
||||
</div>
|
||||
<div
|
||||
</button>
|
||||
<button
|
||||
type='button'
|
||||
className={`${styles.option} ${boardFilter === 'nsfw' && styles.selected}`}
|
||||
role='button'
|
||||
tabIndex={0}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
@@ -113,10 +113,10 @@ const BoardsFilterModal = () => {
|
||||
}}
|
||||
>
|
||||
{t('show_nsfw_boards_only')}
|
||||
</div>
|
||||
<div
|
||||
</button>
|
||||
<button
|
||||
type='button'
|
||||
className={`${styles.option} ${boardFilter === 'worksafe' && styles.selected}`}
|
||||
role='button'
|
||||
tabIndex={0}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
@@ -131,7 +131,7 @@ const BoardsFilterModal = () => {
|
||||
}}
|
||||
>
|
||||
{t('show_worksafe_boards_only')}
|
||||
</div>
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -7,7 +7,7 @@ const BoxModal = () => {
|
||||
const { t } = useTranslation();
|
||||
const [showFilterModal, setShowFilterModal] = useState(false);
|
||||
const modalRef = useRef<HTMLDivElement>(null);
|
||||
const buttonRef = useRef<HTMLSpanElement>(null);
|
||||
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||
|
||||
const { showWorksafeContentOnly, setShowWorksafeContentOnly, showNsfwContentOnly, setShowNsfwContentOnly } = useHomeFiltersStore();
|
||||
|
||||
@@ -45,9 +45,9 @@ const BoxModal = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<span
|
||||
<button
|
||||
type='button'
|
||||
ref={buttonRef}
|
||||
role='button'
|
||||
tabIndex={0}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
@@ -58,36 +58,36 @@ const BoxModal = () => {
|
||||
onClick={() => !showFilterModal && setShowFilterModal(true)}
|
||||
>
|
||||
{t('options')} ▼
|
||||
</span>
|
||||
</button>
|
||||
{showFilterModal && (
|
||||
<div ref={modalRef} className={styles.filterModal}>
|
||||
<div
|
||||
<button
|
||||
type='button'
|
||||
className={`${styles.option} ${showWorksafeContentOnly ? styles.selected : ''}`}
|
||||
role='button'
|
||||
tabIndex={0}
|
||||
onKeyDown={(e) => handleKey(e, 'worksafe')}
|
||||
onClick={() => selectFilter('worksafe')}
|
||||
>
|
||||
{t('show_worksafe_content_only')}
|
||||
</div>
|
||||
<div
|
||||
</button>
|
||||
<button
|
||||
type='button'
|
||||
className={`${styles.option} ${showNsfwContentOnly ? styles.selected : ''}`}
|
||||
role='button'
|
||||
tabIndex={0}
|
||||
onKeyDown={(e) => handleKey(e, 'nsfw')}
|
||||
onClick={() => selectFilter('nsfw')}
|
||||
>
|
||||
{t('show_nsfw_content_only')}
|
||||
</div>
|
||||
<div
|
||||
</button>
|
||||
<button
|
||||
type='button'
|
||||
className={`${styles.option} ${!showWorksafeContentOnly && !showNsfwContentOnly ? styles.selected : ''}`}
|
||||
role='button'
|
||||
tabIndex={0}
|
||||
onKeyDown={(e) => handleKey(e, 'all')}
|
||||
onClick={() => selectFilter('all')}
|
||||
>
|
||||
{t('show_all_content')}
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -57,15 +57,19 @@
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.boxBar span {
|
||||
.boxBar span, .boxBar > button {
|
||||
all: unset;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
padding-right: 0.25em;
|
||||
text-transform: lowercase;
|
||||
font: inherit;
|
||||
color: inherit;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
.boxBar span:hover {
|
||||
.boxBar span:hover, .boxBar > button:hover {
|
||||
cursor: pointer;
|
||||
color: var(--homepage-box-bar-text-color-hover);
|
||||
}
|
||||
@@ -148,7 +152,15 @@
|
||||
}
|
||||
|
||||
.filterModal .option {
|
||||
all: unset;
|
||||
box-sizing: border-box;
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 0 10px;
|
||||
color: inherit;
|
||||
font: inherit;
|
||||
line-height: inherit;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.filterModal .option:hover {
|
||||
@@ -156,6 +168,12 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.boxBar > button:focus-visible,
|
||||
.filterModal .option:focus-visible {
|
||||
outline: 1px solid currentcolor;
|
||||
outline-offset: -1px;
|
||||
}
|
||||
|
||||
.filterModal .selected::before {
|
||||
content: '✓ ';
|
||||
}
|
||||
@@ -389,4 +407,4 @@
|
||||
.footer a:hover {
|
||||
color: var(--homepage-box-bar-text-color-hover);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+13
-2
@@ -44,10 +44,13 @@ const SearchBar = () => {
|
||||
spellCheck='false'
|
||||
autoCapitalize='off'
|
||||
type='text'
|
||||
aria-label={lowerCase(t('enter_board_address'))}
|
||||
placeholder={lowerCase(t('enter_board_address'))}
|
||||
ref={searchInputRef}
|
||||
/>
|
||||
<button className={styles.searchButton}>{t('go')}</button>
|
||||
<button type='submit' className={styles.searchButton}>
|
||||
{t('go')}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
@@ -77,7 +80,15 @@ const InfoBox = () => {
|
||||
i18nKey='no_global_rules_info'
|
||||
shouldUnescape={true}
|
||||
components={{
|
||||
1: <a key='releases-link' href='https://github.com/bitsocialnet/5chan/releases/latest' target='_blank' rel='noopener noreferrer' />,
|
||||
1: (
|
||||
<a
|
||||
key='releases-link'
|
||||
href='https://github.com/bitsocialnet/5chan/releases/latest'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
aria-label='5chan releases'
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
|
||||
@@ -49,8 +49,6 @@ import { PageFooterDesktop, PageFooterMobile, StyleOnlyFooterFirstRow } from '..
|
||||
import footerStyles from '../../components/footer/footer.module.css';
|
||||
import { useModeratedCommunityAddresses } from '../../hooks/use-moderated-community-addresses';
|
||||
|
||||
const { addChallenge } = useChallengesStore.getState();
|
||||
|
||||
/** Path for display: directory code, or full address if has TLD, or shortened for long IPNS keys (no dot) */
|
||||
const getBoardDisplayPath = (address: string, path: string): string => {
|
||||
if (path !== address) return path;
|
||||
@@ -179,7 +177,7 @@ const ModQueueActions = ({ status, error, errorMessage, isPublishing, handleAppr
|
||||
variant === 'row' ? (
|
||||
<span className={styles.buttonWrapper}>
|
||||
[
|
||||
<button className={styles.button} onClick={handleRemove} disabled={isPublishing}>
|
||||
<button type='button' className={styles.button} onClick={handleRemove} disabled={isPublishing}>
|
||||
{t('modQueue.dismiss')}
|
||||
</button>
|
||||
]
|
||||
@@ -187,7 +185,7 @@ const ModQueueActions = ({ status, error, errorMessage, isPublishing, handleAppr
|
||||
) : (
|
||||
<span className={styles.cardRemoveButtonWrapper}>
|
||||
[
|
||||
<button className={styles.cardRemoveButton} onClick={handleRemove} disabled={isPublishing}>
|
||||
<button type='button' className={styles.cardRemoveButton} onClick={handleRemove} disabled={isPublishing}>
|
||||
{t('modQueue.dismiss')}
|
||||
</button>
|
||||
]
|
||||
@@ -239,14 +237,14 @@ const ModQueueActions = ({ status, error, errorMessage, isPublishing, handleAppr
|
||||
<div className={styles.actionButtons}>
|
||||
<span className={styles.buttonWrapper}>
|
||||
[
|
||||
<button className={styles.button} onClick={handleApprove} disabled={isPublishing}>
|
||||
<button type='button' className={styles.button} onClick={handleApprove} disabled={isPublishing}>
|
||||
{t('approve')}
|
||||
</button>
|
||||
]
|
||||
</span>
|
||||
<span className={styles.buttonWrapper}>
|
||||
[
|
||||
<button className={styles.button} onClick={handleReject} disabled={isPublishing}>
|
||||
<button type='button' className={styles.button} onClick={handleReject} disabled={isPublishing}>
|
||||
{t('reject')}
|
||||
</button>
|
||||
]
|
||||
@@ -254,10 +252,10 @@ const ModQueueActions = ({ status, error, errorMessage, isPublishing, handleAppr
|
||||
</div>
|
||||
) : (
|
||||
<div className={styles.cardActions}>
|
||||
<button className={`button ${styles.cardApproveButton}`} onClick={handleApprove} disabled={isPublishing}>
|
||||
<button type='button' className={`button ${styles.cardApproveButton}`} onClick={handleApprove} disabled={isPublishing}>
|
||||
{t('approve')}
|
||||
</button>
|
||||
<button className={`button ${styles.cardRejectButton}`} onClick={handleReject} disabled={isPublishing}>
|
||||
<button type='button' className={`button ${styles.cardRejectButton}`} onClick={handleReject} disabled={isPublishing}>
|
||||
{t('reject')}
|
||||
</button>
|
||||
</div>
|
||||
@@ -286,7 +284,7 @@ const useModQueueActions = (comment: Comment): ModQueueActionState => {
|
||||
communityAddress,
|
||||
commentModeration: approvePendingCommentModeration,
|
||||
onChallenge: async (...args: any) => {
|
||||
addChallenge([...args, comment]);
|
||||
useChallengesStore.getState().addChallenge([...args, comment]);
|
||||
},
|
||||
onChallengeVerification: async (challengeVerification, comment) => {
|
||||
alertChallengeVerificationFailed(challengeVerification, comment);
|
||||
@@ -305,7 +303,7 @@ const useModQueueActions = (comment: Comment): ModQueueActionState => {
|
||||
communityAddress,
|
||||
commentModeration: rejectPendingCommentModeration,
|
||||
onChallenge: async (...args: any) => {
|
||||
addChallenge([...args, comment]);
|
||||
useChallengesStore.getState().addChallenge([...args, comment]);
|
||||
},
|
||||
onChallengeVerification: async (challengeVerification, comment) => {
|
||||
alertChallengeVerificationFailed(challengeVerification, comment);
|
||||
|
||||
@@ -149,7 +149,7 @@ const Pass = () => {
|
||||
<Trans
|
||||
i18nKey='pass_answer_nft'
|
||||
components={{
|
||||
mintPass: <a href='https://github.com/bitsocialnet/mintpass' target='_blank' rel='noopener noreferrer' />,
|
||||
mintPass: <a href='https://github.com/bitsocialnet/mintpass' target='_blank' rel='noopener noreferrer' aria-label='Mintpass' />,
|
||||
}}
|
||||
/>
|
||||
</dd>
|
||||
@@ -158,7 +158,7 @@ const Pass = () => {
|
||||
<Trans
|
||||
i18nKey='pass_answer_proceeds'
|
||||
components={{
|
||||
bitsocial: <a href='https://bitsocial.net' target='_blank' rel='noopener noreferrer' />,
|
||||
bitsocial: <a href='https://bitsocial.net' target='_blank' rel='noopener noreferrer' aria-label='Bitsocial' />,
|
||||
}}
|
||||
/>
|
||||
</dd>
|
||||
|
||||
@@ -34,6 +34,9 @@
|
||||
}
|
||||
|
||||
.postDesktop .hideButton {
|
||||
appearance: none;
|
||||
background-color: transparent;
|
||||
border: 0;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
width: 18px;
|
||||
@@ -42,6 +45,8 @@
|
||||
image-rendering: pixelated;
|
||||
cursor: pointer;
|
||||
transform: translateY(-1px);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.postDesktopHidden {
|
||||
@@ -109,6 +114,12 @@
|
||||
}
|
||||
|
||||
.userAddress {
|
||||
appearance: none;
|
||||
border: 0;
|
||||
display: inline;
|
||||
font: inherit;
|
||||
line-height: inherit;
|
||||
margin: 0;
|
||||
padding: 0 5px;
|
||||
border-radius: 6px;
|
||||
font-size: 0.8em;
|
||||
@@ -144,12 +155,18 @@
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
|
||||
.postDesktop .postNumLink a, .postDesktop .postNumLink span, .replyDesktop .postNumLink a, .replyDesktop .postNumLink span {
|
||||
.postDesktop .postNumLink a, .postDesktop .postNumLink span, .postDesktop .postNumLink button, .replyDesktop .postNumLink a, .replyDesktop .postNumLink span, .replyDesktop .postNumLink button {
|
||||
appearance: none;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
color: unset;
|
||||
font: inherit;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
text-decoration: unset;
|
||||
}
|
||||
|
||||
.postDesktop .postNumLink a:hover, .postDesktop .postNumLink span:hover, .replyDesktop .postNumLink a:hover, .replyDesktop .postNumLink span:hover {
|
||||
.postDesktop .postNumLink a:hover, .postDesktop .postNumLink span:hover, .postDesktop .postNumLink button:hover, .replyDesktop .postNumLink a:hover, .replyDesktop .postNumLink span:hover, .replyDesktop .postNumLink button:hover {
|
||||
color: var(--button-desktop-text-color-hover);
|
||||
}
|
||||
|
||||
@@ -179,7 +196,13 @@
|
||||
}
|
||||
|
||||
.postDesktop .closeMedia {
|
||||
appearance: none;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
color: var(--button-desktop-text-color);
|
||||
font: inherit;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
text-decoration: underline;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
@@ -310,6 +333,9 @@
|
||||
}
|
||||
|
||||
.postDesktop .summary .omittedRepliesButtonWrapper {
|
||||
appearance: none;
|
||||
background-color: transparent;
|
||||
border: 0;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
width: 18px;
|
||||
@@ -317,6 +343,8 @@
|
||||
display: inline-block;
|
||||
image-rendering: pixelated;
|
||||
cursor: pointer;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.postDesktop .summary .hideOmittedReplies {
|
||||
@@ -361,7 +389,7 @@
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.mobileUnhideButton span {
|
||||
.mobileUnhideButton span, .mobileUnhideButton button {
|
||||
padding: 5px 18px 5px;
|
||||
}
|
||||
|
||||
@@ -600,7 +628,14 @@
|
||||
}
|
||||
|
||||
.replyToPost {
|
||||
appearance: none;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.redEditMessage {
|
||||
@@ -695,22 +730,30 @@
|
||||
padding: 1.5px 0;
|
||||
}
|
||||
|
||||
.editedInfo {
|
||||
color: var(--post-mobile-abbr-text-color);
|
||||
font-size: var(--post-mobile-abbr-font-size);
|
||||
}
|
||||
|
||||
.showOriginal {
|
||||
.abbr button {
|
||||
appearance: none;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
color: var(--post-link-text-color);
|
||||
cursor: pointer;
|
||||
display: inline;
|
||||
font: inherit;
|
||||
line-height: inherit;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
text-decoration: var(--post-content-link-text-decoration);
|
||||
}
|
||||
|
||||
.showOriginal:hover {
|
||||
cursor: pointer;
|
||||
.abbr button:hover {
|
||||
color: var(--post-link-text-color-hover);
|
||||
text-decoration: var(--post-content-link-text-decoration-hover);
|
||||
}
|
||||
|
||||
.abbr button:focus-visible {
|
||||
outline: 1px dotted currentcolor;
|
||||
outline-offset: 1px;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.replyQuotePreview {
|
||||
margin-right: 10px;
|
||||
|
||||
@@ -283,7 +283,13 @@ const PostPage = () => {
|
||||
const resolvedCommunityAddress = useResolvedCommunityAddress();
|
||||
const resolvedCommunityIdentifier = useCommunityIdentifier(resolvedCommunityAddress);
|
||||
const isInAllView = isAllView(pathname);
|
||||
const routeState = useMemo(() => getEffectiveRouteUserState(locationState), [locationKey, pathname, locationState]);
|
||||
const routeState = useMemo(() => {
|
||||
// locationKey/pathname are intentional deps: getEffectiveRouteUserState falls back to the
|
||||
// non-reactive window.history.state, so the memo must re-run on every navigation to re-read it.
|
||||
void locationKey;
|
||||
void pathname;
|
||||
return getEffectiveRouteUserState(locationState);
|
||||
}, [locationKey, pathname, locationState]);
|
||||
|
||||
const resolvedComment = useCommentWithFeedCache({ commentCid, autoUpdate: autoUpdateEnabled, community: resolvedCommunityIdentifier });
|
||||
const queuedComment = useMemo(() => getQueuedCommentFromRouteState(routeState, commentCid), [routeState, commentCid]);
|
||||
@@ -370,16 +376,16 @@ const PostPage = () => {
|
||||
const queuedReplyHasMore = queuedReplyRepliesResult.hasMore;
|
||||
const queuedReplyLoadMore = queuedReplyRepliesResult.loadMore;
|
||||
const queuedReplyReset = (queuedReplyRepliesResult as { reset?: () => Promise<void> }).reset;
|
||||
const queuedReplyReplies = (queuedReplyRepliesResult.updatedReplies?.length ? queuedReplyRepliesResult.updatedReplies : queuedReplyRepliesResult.replies) || [];
|
||||
const replyPaginationOverride = useMemo(() => {
|
||||
if (!queuedReply || !post?.cid) return undefined;
|
||||
const queuedReplyReplies = (queuedReplyRepliesResult.updatedReplies?.length ? queuedReplyRepliesResult.updatedReplies : queuedReplyRepliesResult.replies) || [];
|
||||
return {
|
||||
hasMore: queuedReplyHasMore,
|
||||
loadMore: queuedReplyLoadMore,
|
||||
replies: mergeRepliesWithQueuedReply(queuedReplyReplies, queuedReply),
|
||||
reset: queuedReplyReset,
|
||||
};
|
||||
}, [post?.cid, queuedReply, queuedReplyHasMore, queuedReplyLoadMore, queuedReplyReplies, queuedReplyReset]);
|
||||
}, [post?.cid, queuedReply, queuedReplyHasMore, queuedReplyLoadMore, queuedReplyRepliesResult.replies, queuedReplyRepliesResult.updatedReplies, queuedReplyReset]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
|
||||
@@ -63,7 +63,7 @@ const BoardRulesDisplay = ({ communityAddress, directories }: { communityAddress
|
||||
) : rules && rules.length > 0 ? (
|
||||
<ol>
|
||||
{rules.map((rule: string, index: number) => (
|
||||
<li key={index}>
|
||||
<li key={`${index}-${rule}`}>
|
||||
<Markdown content={rule} />
|
||||
</li>
|
||||
))}
|
||||
@@ -117,9 +117,9 @@ const BoardSelector = ({
|
||||
<div className={styles.boxContent}>
|
||||
<div className={styles.selectorRow}>
|
||||
<select value={selectedBoardValue} onChange={handleSelectChange} className={styles.boardSelect}>
|
||||
<option value=''>Select board...</option>
|
||||
{[...directories]
|
||||
.sort((a, b) => getBoardShortCode(a.title).localeCompare(getBoardShortCode(b.title)))
|
||||
<option value=''>Select board…</option>
|
||||
{directories
|
||||
.toSorted((a, b) => getBoardShortCode(a.title).localeCompare(getBoardShortCode(b.title)))
|
||||
.map((sub) => {
|
||||
const shortCode = getBoardShortCode(sub.title);
|
||||
const boardName = getBoardName(sub.title);
|
||||
@@ -134,13 +134,14 @@ const BoardSelector = ({
|
||||
<form onSubmit={handleCustomSubmit} className={styles.customAddressForm}>
|
||||
<input
|
||||
type='text'
|
||||
aria-label={lowerCase(t('enter_board_address'))}
|
||||
placeholder={lowerCase(t('enter_board_address'))}
|
||||
value={customAddress}
|
||||
onChange={(e) => setCustomAddress(e.target.value)}
|
||||
className={styles.addressInput}
|
||||
/>
|
||||
<button type='submit' className={styles.goButton}>
|
||||
Go
|
||||
Open Board
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user