feat: implement 5chan-specific default board list

This commit is contained in:
plebeius
2025-11-03 22:06:18 +01:00
parent 4fd9a9c1a9
commit bae6faa929
13 changed files with 87 additions and 288 deletions
+1 -1
View File
@@ -21,7 +21,7 @@ In the plebbit protocol, a 5chan board is called a _subplebbit_. To run a subple
Peers can connect to your subplebbit using any plebbit client, such as 5chan or Seedit. They only need the subplebbit's address, which is not stored in any central database, as plebbit is a pure peer-to-peer protocol. Peers can connect to your subplebbit using any plebbit client, such as 5chan or Seedit. They only need the subplebbit's address, which is not stored in any central database, as plebbit is a pure peer-to-peer protocol.
### How to add a board to the boards list ### How to add a board to the boards list
The boards list on 5chan is plebbit's [temporary-default-subplebbits](https://github.com/plebbit/temporary-default-subplebbits) list. You can open a pull request in that repo to add your subplebbit to the list, or contact devs via telegram [@plebbit](https://t.me/plebbit). In the future, this process will be automated by submitting proposals to a plebbit DAO, using the [plebbit token](https://etherscan.io/token/0xea81dab2e0ecbc6b5c4172de4c22b6ef6e55bd8f). The boards list on 5chan is plebbit's [lists](https://github.com/plebbit/lists) repository, specifically the [5chan-multisub.json](https://github.com/plebbit/lists/blob/master/5chan-multisub.json) file. You can open a pull request in that repo to add your subplebbit to the list, or contact devs via telegram [@plebbit](https://t.me/plebbit). In the future, this process will be automated by submitting proposals to a plebbit DAO, using the [plebbit token](https://etherscan.io/token/0xea81dab2e0ecbc6b5c4172de4c22b6ef6e55bd8f).
## To run locally ## To run locally
+39 -44
View File
@@ -5,7 +5,6 @@ import { initSnow, removeSnow } from './lib/snow';
import { isAllView, isModView, isSubscriptionsView } from './lib/utils/view-utils'; import { isAllView, isModView, isSubscriptionsView } from './lib/utils/view-utils';
import useReplyModalStore from './stores/use-reply-modal-store'; import useReplyModalStore from './stores/use-reply-modal-store';
import useSpecialThemeStore from './stores/use-special-theme-store'; import useSpecialThemeStore from './stores/use-special-theme-store';
import { useAutoSubscribe } from './hooks/use-auto-subscribe';
import useIsMobile from './hooks/use-is-mobile'; import useIsMobile from './hooks/use-is-mobile';
import useTheme from './hooks/use-theme'; import useTheme from './hooks/use-theme';
import styles from './app.module.css'; import styles from './app.module.css';
@@ -117,54 +116,50 @@ const GlobalLayout = () => {
); );
}; };
const App = () => { const App = () => (
useAutoSubscribe(); <div className={styles.app}>
<Routes>
<Route element={<GlobalLayout />}>
<Route path='/' element={<Home />} />
<Route path='/:tag' element={<Home />} />
<Route path='/faq' element={<FAQ />} />
<Route path='*' element={<NotFound />} />
<Route element={<BoardLayout />}>
<Route path='/p/:subplebbitAddress' element={<Board />} />
<Route path='/p/:subplebbitAddress/settings' element={<Board />} />
<Route path='/p/:subplebbitAddress/catalog' element={<Catalog />} />
<Route path='/p/:subplebbitAddress/catalog/settings' element={<Catalog />} />
return ( <Route path='/p/:subplebbitAddress/c/:commentCid' element={<Post />} />
<div className={styles.app}> <Route path='/p/:subplebbitAddress/c/:commentCid/settings' element={<Post />} />
<Routes> <Route path='/p/:subplebbitAddress/description' element={<Post />} />
<Route element={<GlobalLayout />}> <Route path='/p/:subplebbitAddress/description/settings' element={<Post />} />
<Route path='/' element={<Home />} /> <Route path='/p/:subplebbitAddress/rules' element={<Post />} />
<Route path='/:tag' element={<Home />} /> <Route path='/p/:subplebbitAddress/rules/settings' element={<Post />} />
<Route path='/faq' element={<FAQ />} />
<Route path='*' element={<NotFound />} />
<Route element={<BoardLayout />}>
<Route path='/p/:subplebbitAddress' element={<Board />} />
<Route path='/p/:subplebbitAddress/settings' element={<Board />} />
<Route path='/p/:subplebbitAddress/catalog' element={<Catalog />} />
<Route path='/p/:subplebbitAddress/catalog/settings' element={<Catalog />} />
<Route path='/p/:subplebbitAddress/c/:commentCid' element={<Post />} /> <Route path='/p/all/:timeFilterName?' element={<Board />} />
<Route path='/p/:subplebbitAddress/c/:commentCid/settings' element={<Post />} /> <Route path='/p/all/:timeFilterName?/settings' element={<Board />} />
<Route path='/p/:subplebbitAddress/description' element={<Post />} /> <Route path='/p/all/description' element={<Post />} />
<Route path='/p/:subplebbitAddress/description/settings' element={<Post />} /> <Route path='/p/all/catalog/:timeFilterName?' element={<Catalog />} />
<Route path='/p/:subplebbitAddress/rules' element={<Post />} /> <Route path='/p/all/catalog/:timeFilterName?/settings' element={<Catalog />} />
<Route path='/p/:subplebbitAddress/rules/settings' element={<Post />} />
<Route path='/p/all/:timeFilterName?' element={<Board />} /> <Route path='/p/subscriptions/:timeFilterName?' element={<Board />} />
<Route path='/p/all/:timeFilterName?/settings' element={<Board />} /> <Route path='/p/subscriptions/:timeFilterName?/settings' element={<Board />} />
<Route path='/p/all/description' element={<Post />} /> <Route path='/p/subscriptions/catalog/:timeFilterName?' element={<Catalog />} />
<Route path='/p/all/catalog/:timeFilterName?' element={<Catalog />} /> <Route path='/p/subscriptions/catalog/:timeFilterName?/settings' element={<Catalog />} />
<Route path='/p/all/catalog/:timeFilterName?/settings' element={<Catalog />} />
<Route path='/p/subscriptions/:timeFilterName?' element={<Board />} /> <Route path='/p/mod/:timeFilterName?' element={<Board />} />
<Route path='/p/subscriptions/:timeFilterName?/settings' element={<Board />} /> <Route path='/p/mod/:timeFilterName?/settings' element={<Board />} />
<Route path='/p/subscriptions/catalog/:timeFilterName?' element={<Catalog />} /> <Route path='/p/mod/catalog/:timeFilterName?' element={<Catalog />} />
<Route path='/p/subscriptions/catalog/:timeFilterName?/settings' element={<Catalog />} /> <Route path='/p/mod/catalog/:timeFilterName?/settings' element={<Catalog />} />
<Route path='/p/mod/:timeFilterName?' element={<Board />} /> <Route path='/profile/:accountCommentIndex' element={<PendingPost />} />
<Route path='/p/mod/:timeFilterName?/settings' element={<Board />} /> <Route path='/profile/:accountCommentIndex/settings' element={<PendingPost />} />
<Route path='/p/mod/catalog/:timeFilterName?' element={<Catalog />} />
<Route path='/p/mod/catalog/:timeFilterName?/settings' element={<Catalog />} />
<Route path='/profile/:accountCommentIndex' element={<PendingPost />} />
<Route path='/profile/:accountCommentIndex/settings' element={<PendingPost />} />
</Route>
<Route path='/not-found' element={<NotFound />} />
</Route> </Route>
</Routes> <Route path='/not-found' element={<NotFound />} />
</div> </Route>
); </Routes>
}; </div>
);
export default App; export default App;
-2
View File
@@ -1,2 +0,0 @@
// 5chan shouldn't consider 'vulgar' or 'anti' as nsfw tags, unlike more sfw-oriented clients
export const nsfwTags = ['adult', 'gore'];
-77
View File
@@ -1,77 +0,0 @@
import { useEffect } from 'react';
import { useAccount, setAccount } from '@plebbit/plebbit-react-hooks';
import { getAutoSubscribeAddresses, useDefaultSubplebbits } from './use-default-subplebbits';
import { useAutoSubscribeStore } from '../stores/use-auto-subscribe-store';
const AUTO_SUBSCRIBE_KEY_PREFIX = '5chan-auto-subscribe-done-';
// Keep track of which accounts have been processed globally
const processedAccounts = new Set<string>();
export const useAutoSubscribe = () => {
const account = useAccount();
const accountAddress = account?.author?.address;
const defaultSubplebbits = useDefaultSubplebbits();
const { addCheckingAccount, removeCheckingAccount, isCheckingAccount } = useAutoSubscribeStore();
useEffect(() => {
if (!accountAddress) return;
// Mark as checking immediately when account changes
addCheckingAccount(accountAddress);
const processAutoSubscribe = async () => {
if (!account || !defaultSubplebbits?.length) return;
if (processedAccounts.has(accountAddress)) {
removeCheckingAccount(accountAddress);
return;
}
const storageKey = AUTO_SUBSCRIBE_KEY_PREFIX + accountAddress;
const hasAutoSubscribed = localStorage.getItem(storageKey);
if (account.subscriptions?.length > 0 || hasAutoSubscribed) {
processedAccounts.add(accountAddress);
removeCheckingAccount(accountAddress);
return;
}
const autoSubscribeAddresses = getAutoSubscribeAddresses();
if (autoSubscribeAddresses.length) {
try {
const currentSubscriptions = account.subscriptions || [];
const mergedSubscriptions = [...currentSubscriptions];
for (const address of autoSubscribeAddresses) {
if (!mergedSubscriptions.includes(address)) {
mergedSubscriptions.push(address);
}
}
await setAccount({
...account,
subscriptions: mergedSubscriptions,
});
localStorage.setItem(storageKey, 'true');
} catch (error) {
console.error('Auto-subscribe error:', error);
}
}
processedAccounts.add(accountAddress);
removeCheckingAccount(accountAddress);
};
processAutoSubscribe();
return () => {
if (accountAddress) removeCheckingAccount(accountAddress);
};
}, [account, accountAddress, defaultSubplebbits, addCheckingAccount, removeCheckingAccount]);
return {
isCheckingSubscriptions: !accountAddress || isCheckingAccount(accountAddress),
};
};
+8 -33
View File
@@ -10,10 +10,7 @@ export interface MultisubMetadata {
export interface MultisubSubplebbit { export interface MultisubSubplebbit {
title?: string; title?: string;
address: string; address: string;
tags?: string[]; nsfw?: boolean;
features?: string[];
autoSubscribe5Chan?: boolean;
lowUptime?: boolean;
} }
export interface DefaultSubplebbitsState { export interface DefaultSubplebbitsState {
@@ -24,7 +21,6 @@ export interface DefaultSubplebbitsState {
let cacheSubplebbits: MultisubSubplebbit[] | null = null; let cacheSubplebbits: MultisubSubplebbit[] | null = null;
let cacheMetadata: MultisubMetadata | null = null; let cacheMetadata: MultisubMetadata | null = null;
let cacheAutoSubscribeAddresses: string[] | null = null;
export const useDefaultSubplebbits = () => { export const useDefaultSubplebbits = () => {
const [state, setState] = useState<DefaultSubplebbitsState>({ const [state, setState] = useState<DefaultSubplebbitsState>({
@@ -45,24 +41,17 @@ export const useDefaultSubplebbits = () => {
(async () => { (async () => {
try { try {
const multisub = await fetch('https://raw.githubusercontent.com/plebbit/temporary-default-subplebbits/master/multisub.json').then((res) => { const multisub = await fetch('https://raw.githubusercontent.com/plebbit/lists/master/5chan-multisub.json').then((res) => {
if (!res.ok) { if (!res.ok) {
throw new Error(`HTTP error! status: ${res.status}`); throw new Error(`HTTP error! status: ${res.status}`);
} }
return res.json(); return res.json();
}); });
const filteredSubplebbits = multisub.subplebbits.filter((sub: MultisubSubplebbit) => !sub.lowUptime); cacheSubplebbits = multisub.subplebbits;
cacheSubplebbits = filteredSubplebbits;
// Cache auto-subscribe addresses when we fetch subplebbits
cacheAutoSubscribeAddresses = filteredSubplebbits
.filter((sub: MultisubSubplebbit) => sub.autoSubscribe5Chan && sub.address)
.map((sub: MultisubSubplebbit) => sub.address);
setState({ setState({
subplebbits: filteredSubplebbits, subplebbits: multisub.subplebbits,
loading: false, loading: false,
error: null, error: null,
}); });
@@ -81,8 +70,6 @@ export const useDefaultSubplebbits = () => {
return cacheSubplebbits || state.subplebbits; return cacheSubplebbits || state.subplebbits;
}; };
export const getAutoSubscribeAddresses = () => cacheAutoSubscribeAddresses || [];
export const useDefaultSubplebbitsState = () => { export const useDefaultSubplebbitsState = () => {
const [state, setState] = useState<DefaultSubplebbitsState>({ const [state, setState] = useState<DefaultSubplebbitsState>({
subplebbits: cacheSubplebbits || [], subplebbits: cacheSubplebbits || [],
@@ -102,13 +89,15 @@ export const useDefaultSubplebbitsState = () => {
(async () => { (async () => {
try { try {
const multisub = await fetch('https://raw.githubusercontent.com/plebbit/temporary-default-subplebbits/master/multisub.json').then((res) => { const multisub = await fetch('https://raw.githubusercontent.com/plebbit/lists/master/5chan-multisub.json').then((res) => {
if (!res.ok) { if (!res.ok) {
throw new Error(`HTTP error! status: ${res.status}`); throw new Error(`HTTP error! status: ${res.status}`);
} }
return res.json(); return res.json();
}); });
cacheSubplebbits = multisub.subplebbits;
setState({ setState({
subplebbits: multisub.subplebbits, subplebbits: multisub.subplebbits,
loading: false, loading: false,
@@ -143,7 +132,7 @@ export const useMultisubMetadata = () => {
(async () => { (async () => {
try { try {
const multisub = await fetch( const multisub = await fetch(
'https://raw.githubusercontent.com/plebbit/temporary-default-subplebbits/master/multisub.json', 'https://raw.githubusercontent.com/plebbit/lists/master/5chan-multisub.json',
// { cache: 'no-cache' } // { cache: 'no-cache' }
).then((res) => res.json()); ).then((res) => res.json());
const { title, description, createdAt, updatedAt } = multisub; const { title, description, createdAt, updatedAt } = multisub;
@@ -158,17 +147,3 @@ export const useMultisubMetadata = () => {
return cacheMetadata || metadata; return cacheMetadata || metadata;
}; };
const getUniqueTags = (multisub: any) => {
const allTags = new Set<string>();
Object.values(multisub).forEach((sub: any) => {
if (sub?.tags?.length) {
sub.tags.forEach((tag: string) => allTags.add(tag));
}
});
return Array.from(allTags).sort();
};
export const useDefaultSubplebbitTags = (subplebbits: any) => {
return useMemo(() => getUniqueTags(subplebbits), [subplebbits]);
};
+2 -3
View File
@@ -3,7 +3,6 @@ import { useLocation, useParams } from 'react-router-dom';
import useThemeStore from '../stores/use-theme-store'; import useThemeStore from '../stores/use-theme-store';
import { useDefaultSubplebbits } from './use-default-subplebbits'; import { useDefaultSubplebbits } from './use-default-subplebbits';
import { isAllView, isHomeView, isNotFoundView, isPendingPostView, isSubscriptionsView, isModView } from '../lib/utils/view-utils'; import { isAllView, isHomeView, isNotFoundView, isPendingPostView, isSubscriptionsView, isModView } from '../lib/utils/view-utils';
import { nsfwTags } from '../constants/nsfwTags';
import { useAccountComment } from '@plebbit/plebbit-react-hooks'; import { useAccountComment } from '@plebbit/plebbit-react-hooks';
const useInitialTheme = (pendingPostSubplebbitAddress?: string) => { const useInitialTheme = (pendingPostSubplebbitAddress?: string) => {
@@ -29,7 +28,7 @@ const useInitialTheme = (pendingPostSubplebbitAddress?: string) => {
const subplebbitAddress = pendingPostSubplebbitAddress || pendingPost?.subplebbitAddress; const subplebbitAddress = pendingPostSubplebbitAddress || pendingPost?.subplebbitAddress;
if (subplebbitAddress) { if (subplebbitAddress) {
const subplebbit = subplebbits.find((s) => s.address === subplebbitAddress); const subplebbit = subplebbits.find((s) => s.address === subplebbitAddress);
if (subplebbit && subplebbit.tags && subplebbit.tags.some((tag) => nsfwTags.includes(tag))) { if (subplebbit?.nsfw) {
theme = themes.nsfw || 'yotsuba'; theme = themes.nsfw || 'yotsuba';
} else { } else {
theme = themes.sfw || 'yotsuba-b'; theme = themes.sfw || 'yotsuba-b';
@@ -43,7 +42,7 @@ const useInitialTheme = (pendingPostSubplebbitAddress?: string) => {
theme = 'yotsuba'; theme = 'yotsuba';
} else if (paramsSubplebbitAddress) { } else if (paramsSubplebbitAddress) {
const subplebbit = subplebbits.find((s) => s.address === paramsSubplebbitAddress); const subplebbit = subplebbits.find((s) => s.address === paramsSubplebbitAddress);
if (subplebbit && subplebbit.tags && subplebbit.tags.some((tag) => nsfwTags.includes(tag))) { if (subplebbit?.nsfw) {
theme = themes.nsfw || 'yotsuba'; theme = themes.nsfw || 'yotsuba';
} else { } else {
theme = themes.sfw || 'yotsuba-b'; theme = themes.sfw || 'yotsuba-b';
+14 -3
View File
@@ -22,7 +22,18 @@ const usePopularPosts = (subplebbits: Subplebbit[]) => {
if (subplebbit?.posts?.pages?.hot?.comments) { if (subplebbit?.posts?.pages?.hot?.comments) {
for (const post of Object.values(subplebbit.posts.pages.hot.comments as Comment)) { for (const post of Object.values(subplebbit.posts.pages.hot.comments as Comment)) {
const { deleted, link, linkHeight, linkWidth, locked, pinned, removed, replyCount, thumbnailUrl, timestamp } = post; const {
deleted,
link,
linkHeight,
linkWidth,
locked,
pinned,
removed,
replyCount,
thumbnailUrl,
// timestamp
} = post;
try { try {
const commentMediaInfo = getCommentMediaInfo(link, thumbnailUrl, linkWidth, linkHeight); const commentMediaInfo = getCommentMediaInfo(link, thumbnailUrl, linkWidth, linkHeight);
@@ -30,12 +41,12 @@ const usePopularPosts = (subplebbits: Subplebbit[]) => {
if ( if (
hasThumbnail && hasThumbnail &&
(replyCount > 0 || postsPerSub > 1) && replyCount > 1 &&
!deleted && !deleted &&
!removed && !removed &&
!locked && !locked &&
!pinned && !pinned &&
timestamp > Date.now() / 1000 - 60 * 60 * 24 * 30 && // timestamp > Date.now() / 1000 - 60 * 60 * 24 * 30 &&
!uniqueLinks.has(link) !uniqueLinks.has(link)
) { ) {
subplebbitPosts.push(post); subplebbitPosts.push(post);
+2 -3
View File
@@ -3,7 +3,6 @@ import { useLocation, useParams } from 'react-router-dom';
import { isAllView, isSubscriptionsView, isModView } from '../lib/utils/view-utils'; import { isAllView, isSubscriptionsView, isModView } from '../lib/utils/view-utils';
import useThemeStore from '../stores/use-theme-store'; import useThemeStore from '../stores/use-theme-store';
import { useDefaultSubplebbits } from './use-default-subplebbits'; import { useDefaultSubplebbits } from './use-default-subplebbits';
import { nsfwTags } from '../constants/nsfwTags';
import { useAccountComment } from '@plebbit/plebbit-react-hooks'; import { useAccountComment } from '@plebbit/plebbit-react-hooks';
import useSpecialThemeStore from '../stores/use-special-theme-store'; import useSpecialThemeStore from '../stores/use-special-theme-store';
import { isChristmas } from '../lib/utils/time-utils'; import { isChristmas } from '../lib/utils/time-utils';
@@ -64,7 +63,7 @@ const useTheme = (): [string, (theme: string) => void] => {
storedTheme = themes.sfw; storedTheme = themes.sfw;
} else if (subplebbitAddress) { } else if (subplebbitAddress) {
const subplebbit = subplebbits.find((s) => s.address === subplebbitAddress); const subplebbit = subplebbits.find((s) => s.address === subplebbitAddress);
if (subplebbit && subplebbit.tags && subplebbit.tags.some((tag) => nsfwTags.includes(tag))) { if (subplebbit?.nsfw) {
storedTheme = themes.nsfw; storedTheme = themes.nsfw;
} else { } else {
storedTheme = themes.sfw; storedTheme = themes.sfw;
@@ -85,7 +84,7 @@ const useTheme = (): [string, (theme: string) => void] => {
await setThemeStore('sfw', newTheme); await setThemeStore('sfw', newTheme);
} else if (subplebbitAddress) { } else if (subplebbitAddress) {
const subplebbit = subplebbits.find((s) => s.address === subplebbitAddress); const subplebbit = subplebbits.find((s) => s.address === subplebbitAddress);
if (subplebbit && subplebbit.tags && subplebbit.tags.some((tag) => nsfwTags.includes(tag))) { if (subplebbit?.nsfw) {
await setThemeStore('nsfw', newTheme); await setThemeStore('nsfw', newTheme);
} else { } else {
await setThemeStore('sfw', newTheme); await setThemeStore('sfw', newTheme);
-25
View File
@@ -1,25 +0,0 @@
import { create } from 'zustand';
interface AutoSubscribeState {
checkingAccounts: Set<string>;
addCheckingAccount: (address: string) => void;
removeCheckingAccount: (address: string) => void;
isCheckingAccount: (address: string) => boolean;
reset: () => void;
}
export const useAutoSubscribeStore = create<AutoSubscribeState>((set, get) => ({
checkingAccounts: new Set(),
addCheckingAccount: (address) =>
set((state) => ({
checkingAccounts: new Set(state.checkingAccounts).add(address),
})),
removeCheckingAccount: (address) =>
set((state) => {
const newSet = new Set(state.checkingAccounts);
newSet.delete(address);
return { checkingAccounts: newSet };
}),
isCheckingAccount: (address) => get().checkingAccounts.has(address),
reset: () => set({ checkingAccounts: new Set() }),
}));
+4 -7
View File
@@ -129,7 +129,7 @@ const Catalog = () => {
const isInAllView = isAllView(location.pathname); const isInAllView = isAllView(location.pathname);
const defaultSubplebbits = useDefaultSubplebbits(); const defaultSubplebbits = useDefaultSubplebbits();
const { hideAdultBoards, hideGoreBoards } = useInterfaceSettingsStore(); const { hideAdultBoards } = useInterfaceSettingsStore();
const { showTextOnlyThreads, filterItems, searchText, clearMatchedFilters } = useCatalogFiltersStore(); const { showTextOnlyThreads, filterItems, searchText, clearMatchedFilters } = useCatalogFiltersStore();
const account = useAccount(); const account = useAccount();
@@ -139,11 +139,8 @@ const Catalog = () => {
const subplebbitAddresses = useMemo(() => { const subplebbitAddresses = useMemo(() => {
const filteredDefaultSubplebbits = defaultSubplebbits const filteredDefaultSubplebbits = defaultSubplebbits
.filter((subplebbit) => { .filter((subplebbit) => {
const { tags } = subplebbit; // Hide NSFW boards if hideAdultBoards is enabled (treating NSFW as adult content)
const hasGoreTag = tags?.includes('gore'); if (subplebbit.nsfw && hideAdultBoards) {
const hasAdultTag = tags?.includes('adult');
if ((hasGoreTag && hideGoreBoards) || (hasAdultTag && hideAdultBoards)) {
return false; return false;
} }
return true; return true;
@@ -159,7 +156,7 @@ const Catalog = () => {
} }
// Only include subplebbitAddress if it's defined // Only include subplebbitAddress if it's defined
return subplebbitAddress ? [subplebbitAddress] : []; return subplebbitAddress ? [subplebbitAddress] : [];
}, [isInAllView, isInSubscriptionsView, subplebbitAddress, defaultSubplebbits, subscriptions, hideAdultBoards, hideGoreBoards]); }, [isInAllView, isInSubscriptionsView, subplebbitAddress, defaultSubplebbits, subscriptions, hideAdultBoards]);
const { imageSize } = useCatalogStyleStore(); const { imageSize } = useCatalogStyleStore();
const columnWidth = imageSize === 'Large' ? 270 : 180; const columnWidth = imageSize === 'Large' ? 270 : 180;
+12 -85
View File
@@ -1,95 +1,38 @@
import { useState, useEffect } from 'react'; import { useState } from 'react';
import { Link, useLocation } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import Plebbit from '@plebbit/plebbit-js'; import Plebbit from '@plebbit/plebbit-js';
import { Subplebbit, useSubplebbitStats } from '@plebbit/plebbit-react-hooks'; import { useDefaultSubplebbitsState, MultisubSubplebbit } from '../../../hooks/use-default-subplebbits';
import useSubplebbitsStore from '@plebbit/plebbit-react-hooks/dist/stores/subplebbits';
import { useDefaultSubplebbitsState, useDefaultSubplebbitTags } from '../../../hooks/use-default-subplebbits';
import useIsMobile from '../../../hooks/use-is-mobile'; import useIsMobile from '../../../hooks/use-is-mobile';
import useIsSubplebbitOffline from '../../../hooks/use-is-subplebbit-offline';
import LoadingEllipsis from '../../../components/loading-ellipsis'; import LoadingEllipsis from '../../../components/loading-ellipsis';
import Tooltip from '../../../components/tooltip';
import styles from './boards-list.module.css'; import styles from './boards-list.module.css';
import { nsfwTags } from '../../../constants/nsfwTags';
const Board = ({ subplebbit, isMobile }: { subplebbit: Subplebbit; isMobile: boolean }) => { const Board = ({ subplebbit, isMobile }: { subplebbit: MultisubSubplebbit; isMobile: boolean }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { address, tags } = subplebbit || {}; const { address, title, nsfw } = subplebbit || {};
const nsfwTag = tags?.find((tag: string) => nsfwTags.includes(tag));
let stats = useSubplebbitStats({ subplebbitAddress: address });
const subplebbitData = useSubplebbitsStore((state) => state.subplebbits[address]);
const { isOffline, isOnlineStatusLoading, offlineIconClass, offlineTitle } = useIsSubplebbitOffline(subplebbitData);
const displayAddress = address && Plebbit.getShortAddress(address); const displayAddress = address && Plebbit.getShortAddress(address);
const showOfflineIcon = address && (isOffline || isOnlineStatusLoading);
const title =
subplebbitData?.title ||
(subplebbitData?.updatedAt ? (
displayAddress.endsWith('.eth') || displayAddress.endsWith('.sol') ? (
displayAddress.slice(0, -4)
) : (
displayAddress
)
) : (
<span className={styles.loadingBoardCellValue}>{isOffline ? t('board_not_reachable') : t('loading_board')}</span>
));
return ( return (
<tr key={address}> <tr key={address}>
<td> <td>
<p className={styles.boardCell}> <p className={styles.boardCell}>
<Link to={`/p/${address}`}>{displayAddress}</Link> <Link to={`/p/${address}`}>{displayAddress}</Link>
{nsfwTag && <span className={styles.nsfw}> ({t(nsfwTag)})</span>} {nsfw && <span className={styles.nsfw}> ({t('nsfw')})</span>}
{showOfflineIcon && (
<span className={styles.offlineIconContainer}>
<Tooltip content={offlineTitle}>
<span className={`${styles.offlineIcon} ${offlineIconClass}`} />
</Tooltip>
</span>
)}
</p> </p>
</td> </td>
<td> <td>
<p className={styles.boardCell}>{title}</p> <p className={styles.boardCell}>{title || displayAddress}</p>
</td> </td>
{!isMobile && (
<>
<td className={styles.boardPPH}>
<p className={styles.boardCell}>{stats.hourPostCount ?? <span className={styles.loadingBoardCellValue}>?</span>}</p>
</td>
<td className={styles.boardTags}>
<p className={styles.boardCell}>
{tags.map((tag: string, index: number) => (
<span key={tag}>
<Link to={`/${tag}`}>{tag}</Link>
{index < tags.length - 1 && ', '}
</span>
))}
</p>
</td>
</>
)}
</tr> </tr>
); );
}; };
const BoardsList = ({ multisub }: { multisub: Subplebbit[] }) => { const BoardsList = ({ multisub }: { multisub: MultisubSubplebbit[] }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const location = useLocation();
const [displayCount, setDisplayCount] = useState(15); const [displayCount, setDisplayCount] = useState(15);
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const { loading, error } = useDefaultSubplebbitsState(); const { loading, error } = useDefaultSubplebbitsState();
const currentTag = location.pathname.split('/').filter(Boolean)[0];
const tags = useDefaultSubplebbitTags(multisub);
useEffect(() => {
setDisplayCount(15);
}, [currentTag]);
if (loading) { if (loading) {
return ( return (
<div className={styles.boardsBox}> <div className={styles.boardsBox}>
@@ -108,12 +51,9 @@ const BoardsList = ({ multisub }: { multisub: Subplebbit[] }) => {
); );
} }
const filteredBoards = (currentTag && tags.includes(currentTag) ? multisub.filter((sub) => sub?.tags?.includes(currentTag)) : multisub).slice(0, displayCount); const filteredBoards = multisub.slice(0, displayCount);
const totalBoardCount = multisub.length;
const totalBoardCount = currentTag && tags.includes(currentTag) ? multisub.filter((sub) => sub?.tags?.includes(currentTag)).length : multisub.length; const hasMoreBoards = displayCount < multisub.length;
const hasMoreBoards =
currentTag && tags.includes(currentTag) ? displayCount < multisub.filter((sub) => sub?.tags?.includes(currentTag)).length : displayCount < multisub.length;
return ( return (
<div className={styles.boardsBox}> <div className={styles.boardsBox}>
@@ -126,12 +66,6 @@ const BoardsList = ({ multisub }: { multisub: Subplebbit[] }) => {
<tr> <tr>
<th>{t('board')}</th> <th>{t('board')}</th>
<th>{t('title')}</th> <th>{t('title')}</th>
{!isMobile && (
<th className={styles.boardPPH} title={t('pph')}>
PPH
</th>
)}
{!isMobile && <th>{t('tags')}</th>}
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -141,14 +75,7 @@ const BoardsList = ({ multisub }: { multisub: Subplebbit[] }) => {
</tbody> </tbody>
</table> </table>
<div className={styles.displayCount}> <div className={styles.displayCount}>
{currentTag && tags.includes(currentTag) {t('displaying_boards', { filteredBoardsCount: filteredBoards.length, totalBoardCount: totalBoardCount, interpolation: { escapeValue: false } })}
? t('displaying_boards_with_tag', {
filteredBoardsCount: filteredBoards.length,
totalBoardCount: totalBoardCount,
currentTag: `"${currentTag}"`,
interpolation: { escapeValue: false },
})
: t('displaying_boards', { filteredBoardsCount: filteredBoards.length, totalBoardCount: totalBoardCount, interpolation: { escapeValue: false } })}
</div> </div>
{hasMoreBoards && ( {hasMoreBoards && (
<div className={styles.loadMoreButton}> <div className={styles.loadMoreButton}>
+1 -1
View File
@@ -10,7 +10,7 @@ import BoardsList from './boards-list';
import Version from '../../components/version'; import Version from '../../components/version';
import _ from 'lodash'; import _ from 'lodash';
// https://github.com/plebbit/temporary-default-subplebbits/blob/master/README.md // https://github.com/plebbit/lists/blob/master/5chan-multisub.json
const SearchBar = () => { const SearchBar = () => {
const searchInputRef = useRef<HTMLInputElement>(null); const searchInputRef = useRef<HTMLInputElement>(null);
@@ -9,7 +9,7 @@ import { getCommentMediaInfo } from '../../../lib/utils/media-utils';
import { CatalogPostMedia } from '../../../components/catalog-row'; import { CatalogPostMedia } from '../../../components/catalog-row';
import LoadingEllipsis from '../../../components/loading-ellipsis'; import LoadingEllipsis from '../../../components/loading-ellipsis';
import BoxModal from '../box-modal'; import BoxModal from '../box-modal';
import { nsfwTags } from '../../../constants/nsfwTags'; import { MultisubSubplebbit } from '../../../hooks/use-default-subplebbits';
import { removeMarkdown } from '../../../lib/utils/post-utils'; import { removeMarkdown } from '../../../lib/utils/post-utils';
interface PopularThreadProps { interface PopularThreadProps {
@@ -50,7 +50,7 @@ const PopularThreadCard = ({ post, boardTitle, boardShortAddress }: PopularThrea
); );
}; };
const PopularThreadsBox = ({ multisub, subplebbits }: { multisub: Subplebbit[]; subplebbits: any }) => { const PopularThreadsBox = ({ multisub, subplebbits }: { multisub: MultisubSubplebbit[]; subplebbits: any }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { showWorksafeContentOnly, showNsfwContentOnly } = usePopularThreadsOptionsStore(); const { showWorksafeContentOnly, showNsfwContentOnly } = usePopularThreadsOptionsStore();
@@ -58,13 +58,13 @@ const PopularThreadsBox = ({ multisub, subplebbits }: { multisub: Subplebbit[];
if (showWorksafeContentOnly) { if (showWorksafeContentOnly) {
return subplebbits.filter((sub: Subplebbit) => { return subplebbits.filter((sub: Subplebbit) => {
const multisubEntry = multisub.find((ms) => ms?.address === sub?.address); const multisubEntry = multisub.find((ms) => ms?.address === sub?.address);
return multisubEntry ? !multisubEntry.tags.some((tag: string) => nsfwTags.includes(tag)) : true; return multisubEntry ? !multisubEntry.nsfw : true;
}); });
} }
if (showNsfwContentOnly) { if (showNsfwContentOnly) {
return subplebbits.filter((sub: Subplebbit) => { return subplebbits.filter((sub: Subplebbit) => {
const multisubEntry = multisub.find((ms) => ms?.address === sub?.address); const multisubEntry = multisub.find((ms) => ms?.address === sub?.address);
return multisubEntry ? multisubEntry.tags.some((tag: string) => nsfwTags.includes(tag)) : false; return multisubEntry ? multisubEntry.nsfw : false;
}); });
} }
return subplebbits; return subplebbits;