From cadfc699fcf5b9370bdddd04f6e7261703923cca Mon Sep 17 00:00:00 2001 From: "Tom (plebeius.eth)" Date: Mon, 3 Feb 2025 13:08:24 +0100 Subject: [PATCH] feat(home): add boards list more similar to vichan, which is better than 4chan's boards box for a potentially infinite number of boards --- src/hooks/use-default-subplebbits.ts | 25 +-- src/stores/use-home-filters-store.ts | 44 ----- .../use-popular-threads-options-store.ts | 23 +++ src/views/home/boards-box/boards-box.tsx | 164 ------------------ src/views/home/boards-box/index.ts | 1 - src/views/home/boards-list/boards-list.tsx | 63 +++++++ src/views/home/boards-list/index.ts | 1 + src/views/home/box-modal/box-modal.tsx | 138 ++++----------- src/views/home/home.module.css | 106 +++++------ src/views/home/home.tsx | 4 +- .../popular-threads-box.tsx | 6 +- 11 files changed, 186 insertions(+), 389 deletions(-) delete mode 100644 src/stores/use-home-filters-store.ts create mode 100644 src/stores/use-popular-threads-options-store.ts delete mode 100644 src/views/home/boards-box/boards-box.tsx delete mode 100644 src/views/home/boards-box/index.ts create mode 100644 src/views/home/boards-list/boards-list.tsx create mode 100644 src/views/home/boards-list/index.ts diff --git a/src/hooks/use-default-subplebbits.ts b/src/hooks/use-default-subplebbits.ts index 8407f4fa..f7aaa81e 100644 --- a/src/hooks/use-default-subplebbits.ts +++ b/src/hooks/use-default-subplebbits.ts @@ -41,32 +41,9 @@ const useDefaultSubplebbits = () => { return cacheSubplebbits || subplebbits; }; -export const categorizeSubplebbits = (subplebbits: MultisubSubplebbit[]) => { - const plebbitSubs = subplebbits.filter((sub) => sub.tags?.includes('plebbit')); - const interestsSubs = subplebbits.filter( - (sub) => sub.tags?.includes('topic') && !sub.tags?.includes('plebbit') && !sub.tags?.includes('country') && !sub.tags?.includes('international'), - ); - const randomSubs = subplebbits.filter((sub) => sub.tags?.includes('random') && !sub.tags?.includes('plebbit')); - const internationalSubs = subplebbits.filter((sub) => sub.tags?.includes('international') || sub.tags?.includes('country')); - const projectsSubs = subplebbits.filter((sub) => sub.tags?.includes('project') && !sub.tags?.includes('plebbit') && !sub.tags?.includes('topic')); - - return { plebbitSubs, interestsSubs, randomSubs, internationalSubs, projectsSubs }; -}; - export const useDefaultSubplebbitAddresses = () => { const defaultSubplebbits = useDefaultSubplebbits(); - const categorizedSubplebbits = useMemo(() => categorizeSubplebbits(defaultSubplebbits), [defaultSubplebbits]); - return useMemo( - () => - [ - ...categorizedSubplebbits.plebbitSubs, - ...categorizedSubplebbits.projectsSubs, - ...categorizedSubplebbits.interestsSubs, - ...categorizedSubplebbits.randomSubs, - ...categorizedSubplebbits.internationalSubs, - ].map((subplebbit) => subplebbit.address), - [categorizedSubplebbits], - ); + return useMemo(() => defaultSubplebbits.map((subplebbit) => subplebbit.address), [defaultSubplebbits]); }; export const useMultisubMetadata = () => { diff --git a/src/stores/use-home-filters-store.ts b/src/stores/use-home-filters-store.ts deleted file mode 100644 index 00fed51c..00000000 --- a/src/stores/use-home-filters-store.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { create } from 'zustand'; - -interface HomeFiltersStore { - showNsfwBoardsOnly: boolean; - setShowNsfwBoardsOnly: (value: boolean) => void; - showWorksafeBoardsOnly: boolean; - setShowWorksafeBoardsOnly: (value: boolean) => void; - useCatalog: boolean; - setUseCatalog: (value: boolean) => void; - showWorksafeContentOnly: boolean; - setShowWorksafeContentOnly: (value: boolean) => void; - showNsfwContentOnly: boolean; - setShowNsfwContentOnly: (value: boolean) => void; -} - -const useHomeFiltersStore = create((set) => ({ - showNsfwBoardsOnly: localStorage.getItem('showNsfwBoardsOnly') === 'true' ? true : false, - setShowNsfwBoardsOnly: (value: boolean) => { - set({ showNsfwBoardsOnly: value }); - localStorage.setItem('showNsfwBoardsOnly', value.toString()); - }, - showWorksafeBoardsOnly: localStorage.getItem('showWorksafeBoardsOnly') === 'true' ? true : false, - setShowWorksafeBoardsOnly: (value: boolean) => { - set({ showWorksafeBoardsOnly: value }); - localStorage.setItem('showWorksafeBoardsOnly', value.toString()); - }, - useCatalog: localStorage.getItem('useCatalog') === 'true' ? true : false, - setUseCatalog: (value: boolean) => { - set({ useCatalog: value }); - localStorage.setItem('useCatalog', value.toString()); - }, - showWorksafeContentOnly: localStorage.getItem('showWorksafeContentOnly') === 'true' || localStorage.getItem('showWorksafeContentOnly') === null ? true : false, - setShowWorksafeContentOnly: (value: boolean) => { - set({ showWorksafeContentOnly: value }); - localStorage.setItem('showWorksafeContentOnly', value.toString()); - }, - showNsfwContentOnly: localStorage.getItem('showNsfwContentOnly') === 'true' ? true : false, - setShowNsfwContentOnly: (value: boolean) => { - set({ showNsfwContentOnly: value }); - localStorage.setItem('showNsfwContentOnly', value.toString()); - }, -})); - -export default useHomeFiltersStore; diff --git a/src/stores/use-popular-threads-options-store.ts b/src/stores/use-popular-threads-options-store.ts new file mode 100644 index 00000000..377fdc4d --- /dev/null +++ b/src/stores/use-popular-threads-options-store.ts @@ -0,0 +1,23 @@ +import { create } from 'zustand'; + +interface PopularThreadsOptionsStore { + showWorksafeContentOnly: boolean; + setShowWorksafeContentOnly: (value: boolean) => void; + showNsfwContentOnly: boolean; + setShowNsfwContentOnly: (value: boolean) => void; +} + +const usePopularThreadsOptionsStore = create((set) => ({ + showWorksafeContentOnly: localStorage.getItem('showWorksafeContentOnly') === 'true' || localStorage.getItem('showWorksafeContentOnly') === null ? true : false, + setShowWorksafeContentOnly: (value: boolean) => { + set({ showWorksafeContentOnly: value }); + localStorage.setItem('showWorksafeContentOnly', value.toString()); + }, + showNsfwContentOnly: localStorage.getItem('showNsfwContentOnly') === 'true' ? true : false, + setShowNsfwContentOnly: (value: boolean) => { + set({ showNsfwContentOnly: value }); + localStorage.setItem('showNsfwContentOnly', value.toString()); + }, +})); + +export default usePopularThreadsOptionsStore; diff --git a/src/views/home/boards-box/boards-box.tsx b/src/views/home/boards-box/boards-box.tsx deleted file mode 100644 index 9b108dc2..00000000 --- a/src/views/home/boards-box/boards-box.tsx +++ /dev/null @@ -1,164 +0,0 @@ -import { Link } from 'react-router-dom'; -import { useTranslation } from 'react-i18next'; -import { Subplebbit, useAccount, useAccountSubplebbits } from '@plebbit/plebbit-react-hooks'; -import Plebbit from '@plebbit/plebbit-js/dist/browser/index.js'; -import useHomeFiltersStore from '../../../stores/use-home-filters-store'; -import { useMultisubMetadata } from '../../../hooks/use-default-subplebbits'; -import styles from '../home.module.css'; -import { nsfwTags } from '../home'; -import BoxModal from '../box-modal'; -import useIsSubplebbitOffline from '../../../hooks/use-is-subplebbit-offline'; - -const Board = ({ subplebbit, subplebbits, useCatalog }: { subplebbit: Subplebbit; subplebbits: any; useCatalog?: boolean }) => { - const { t } = useTranslation(); - const { address, title, tags } = subplebbit || {}; - const nsfwTag = tags.find((tag: string) => nsfwTags.includes(tag)); - - const boardLink = useCatalog ? `/p/${address}/catalog` : `/p/${address}`; - - const subplebbitData = subplebbits && subplebbits.find((sub: Subplebbit) => sub?.address === address); - const { isOffline, isOnlineStatusLoading, offlineIconClass, offlineTitle } = useIsSubplebbitOffline(subplebbitData); - - return ( -
- {(isOffline || isOnlineStatusLoading) && } - {title || (address.endsWith('.eth') || address.endsWith('.sol') ? address.slice(0, -4) : address && Plebbit.getShortAddress(address))} - {nsfwTag && ({t(nsfwTag)})} -
- ); -}; - -const BoardsBox = ({ multisub, subplebbits }: { multisub: Subplebbit[]; subplebbits: any }) => { - const { t } = useTranslation(); - const account = useAccount(); - const subscriptions = account?.subscriptions || []; - const { accountSubplebbits } = useAccountSubplebbits(); - const accountSubplebbitAddresses = Object.keys(accountSubplebbits); - const { showNsfwBoardsOnly, showWorksafeBoardsOnly } = useHomeFiltersStore(); - - const { useCatalog } = useHomeFiltersStore(); - - const filterSubs = (subs: Subplebbit[], includeTags: string[], excludeTags: string[] = []) => - subs.filter((sub) => includeTags.every((tag) => sub.tags?.includes(tag)) && excludeTags.every((tag) => !sub.tags?.includes(tag))); - - let plebbitSubs = filterSubs(multisub, ['plebbit']); - let interestsSubs = filterSubs(multisub, ['topic'], ['plebbit', 'country', 'international']); - let randomSubs = filterSubs(multisub, ['random'], ['plebbit']); - let internationalSubs = filterSubs(multisub, ['international']); - let projectsSubs = filterSubs(multisub, ['project'], ['plebbit', 'topic']); - - const filterByNsfw = (subs: Subplebbit[], includeNsfw: boolean) => subs.filter((sub) => sub.tags.some((tag: string) => nsfwTags.includes(tag)) === includeNsfw); - - const filterCategoriesByNsfw = (subsArray: Subplebbit[][], includeNsfw: boolean) => subsArray.map((subs) => filterByNsfw(subs, includeNsfw)); - - const includeNsfw = showNsfwBoardsOnly ? true : showWorksafeBoardsOnly ? false : null; - - if (includeNsfw !== null) { - [plebbitSubs, interestsSubs, randomSubs, internationalSubs, projectsSubs] = filterCategoriesByNsfw( - [plebbitSubs, interestsSubs, randomSubs, internationalSubs, projectsSubs], - includeNsfw, - ); - } - - const multisubMetadata = useMultisubMetadata(); - - return ( -
-
-

{t('boards')}

- -
-
-
-

Multiboards

-
-
- {multisubMetadata?.title || 'All'} -
-
- Subscriptions -
-
- {plebbitSubs.length > 0 && ( - <> -

Plebbit

-
- {plebbitSubs.map((sub) => ( - - ))} -
- - )} - {projectsSubs.length > 0 && ( - <> -

{t('projects')}

-
- {projectsSubs.map((sub) => ( - - ))} -
- - )} -
-
- {interestsSubs.length > 0 && ( - <> -

{t('interests')}

-
- {interestsSubs.map((sub) => ( - - ))} -
- - )} -
-
- {randomSubs.length > 0 && ( - <> -

{t('random')}

-
- {randomSubs.map((sub) => ( - - ))} -
- - )} - {internationalSubs.length > 0 && ( - <> -

{t('international')}

-
- {internationalSubs.map((sub) => ( - - ))} -
- - )} -
-
-
-

{t('subscriptions')}

- {subscriptions.length > 0 - ? subscriptions.map((address: string, index: number) => ( -
- p/{address && Plebbit.getShortAddress(address)} -
- )) - : t('not_subscribed')} -
-
-

{t('moderating')}

- {accountSubplebbitAddresses.length > 0 - ? accountSubplebbitAddresses.map((address: string, index: number) => ( -
- p/{address && Plebbit.getShortAddress(address)} -
- )) - : t('not_moderating')} -
-
-
-
- ); -}; - -export default BoardsBox; diff --git a/src/views/home/boards-box/index.ts b/src/views/home/boards-box/index.ts deleted file mode 100644 index 4482f675..00000000 --- a/src/views/home/boards-box/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from './boards-box'; diff --git a/src/views/home/boards-list/boards-list.tsx b/src/views/home/boards-list/boards-list.tsx new file mode 100644 index 00000000..9fe31427 --- /dev/null +++ b/src/views/home/boards-list/boards-list.tsx @@ -0,0 +1,63 @@ +import { Link } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; +import Plebbit from '@plebbit/plebbit-js/dist/browser/index.js'; +import { Subplebbit } from '@plebbit/plebbit-react-hooks'; +import useIsSubplebbitOffline from '../../../hooks/use-is-subplebbit-offline'; +import styles from '../home.module.css'; +import { nsfwTags } from '../home'; + +const Board = ({ subplebbit, subplebbits }: { subplebbit: Subplebbit; subplebbits: any; useCatalog?: boolean }) => { + const { t } = useTranslation(); + const { address, title, tags } = subplebbit || {}; + const nsfwTag = tags.find((tag: string) => nsfwTags.includes(tag)); + + const subplebbitData = subplebbits && subplebbits.find((sub: Subplebbit) => sub?.address === address); + const { isOffline, isOnlineStatusLoading, offlineIconClass, offlineTitle } = useIsSubplebbitOffline(subplebbitData); + + return ( + + +

+ {address && Plebbit.getShortAddress(address)} + {(isOffline || isOnlineStatusLoading) && } + {nsfwTag && ({t(nsfwTag)})} +

+ + +

{title || (address && Plebbit.getShortAddress(address))}

+ + +

{tags.join(', ')}

+ + + ); +}; + +const BoardsList = ({ multisub, subplebbits }: { multisub: Subplebbit[]; subplebbits: any }) => { + const { t } = useTranslation(); + + return ( +
+ + + + + + + + + + + + + + {multisub.map((sub) => ( + + ))} + +
{t('board')}{t('title')}{t('tags')}
+
+ ); +}; + +export default BoardsList; diff --git a/src/views/home/boards-list/index.ts b/src/views/home/boards-list/index.ts new file mode 100644 index 00000000..9eceeca1 --- /dev/null +++ b/src/views/home/boards-list/index.ts @@ -0,0 +1 @@ +export { default } from './boards-list'; diff --git a/src/views/home/box-modal/box-modal.tsx b/src/views/home/box-modal/box-modal.tsx index 3a76b146..5c34c338 100644 --- a/src/views/home/box-modal/box-modal.tsx +++ b/src/views/home/box-modal/box-modal.tsx @@ -1,26 +1,15 @@ import { useCallback, useEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import useHomeFiltersStore from '../../../stores/use-home-filters-store'; +import useHomeFiltersStore from '../../../stores/use-popular-threads-options-store'; import styles from '../home.module.css'; -const BoxModal = ({ isBoardsBoxModal }: { isBoardsBoxModal: boolean }) => { +const BoxModal = () => { const { t } = useTranslation(); const [showFilterModal, setShowFilterModal] = useState(false); const modalRef = useRef(null); const buttonRef = useRef(null); - const { - showNsfwBoardsOnly, - setShowNsfwBoardsOnly, - showWorksafeBoardsOnly, - setShowWorksafeBoardsOnly, - useCatalog, - setUseCatalog, - showWorksafeContentOnly, - setShowWorksafeContentOnly, - showNsfwContentOnly, - setShowNsfwContentOnly, - } = useHomeFiltersStore(); + const { showWorksafeContentOnly, setShowWorksafeContentOnly, showNsfwContentOnly, setShowNsfwContentOnly } = useHomeFiltersStore(); const handleClickOutside = useCallback( (event: MouseEvent) => { @@ -41,95 +30,44 @@ const BoxModal = ({ isBoardsBoxModal }: { isBoardsBoxModal: boolean }) => { return ( <> !showFilterModal && setShowFilterModal(true)}> - {isBoardsBoxModal ? t('filter') : t('options')} ▼ + {t('options')} ▼ {showFilterModal && (
- {isBoardsBoxModal ? ( - <> -
{ - setShowNsfwBoardsOnly(false); - setShowWorksafeBoardsOnly(false); - setShowFilterModal(false); - }} - > - {t('show_all_boards')} -
-
{ - if (showWorksafeBoardsOnly) { - setShowWorksafeBoardsOnly(false); - } - setShowNsfwBoardsOnly(!showNsfwBoardsOnly); - setShowFilterModal(false); - }} - > - {t('show_nsfw_boards_only')} -
-
{ - if (showNsfwBoardsOnly) { - setShowNsfwBoardsOnly(false); - } - setShowWorksafeBoardsOnly(!showWorksafeBoardsOnly); - setShowFilterModal(false); - }} - > - {t('show_worksafe_boards_only')} -
-
-
{ - setUseCatalog(!useCatalog); - setShowFilterModal(false); - }} - > - {t('use_catalog')} -
- - ) : ( - <> -
{ - if (showNsfwContentOnly) { - setShowNsfwContentOnly(false); - } - setShowWorksafeContentOnly(!showWorksafeContentOnly); - setShowFilterModal(false); - }} - > - {t('show_worksafe_content_only')} -
-
{ - if (showWorksafeContentOnly) { - setShowWorksafeContentOnly(false); - } - setShowNsfwContentOnly(!showNsfwContentOnly); - setShowFilterModal(false); - }} - > - {t('show_nsfw_content_only')} -
-
{ - setShowWorksafeContentOnly(false); - setShowNsfwContentOnly(false); - setShowFilterModal(false); - }} - > - {t('show_all_content')} -
- - )} +
{ + if (showNsfwContentOnly) { + setShowNsfwContentOnly(false); + } + setShowWorksafeContentOnly(!showWorksafeContentOnly); + setShowFilterModal(false); + }} + > + {t('show_worksafe_content_only')} +
+
{ + if (showWorksafeContentOnly) { + setShowWorksafeContentOnly(false); + } + setShowNsfwContentOnly(!showNsfwContentOnly); + setShowFilterModal(false); + }} + > + {t('show_nsfw_content_only')} +
+
{ + setShowWorksafeContentOnly(false); + setShowNsfwContentOnly(false); + setShowFilterModal(false); + }} + > + {t('show_all_content')} +
)} diff --git a/src/views/home/home.module.css b/src/views/home/home.module.css index 96cc7eca..2a51c416 100644 --- a/src/views/home/home.module.css +++ b/src/views/home/home.module.css @@ -34,6 +34,61 @@ min-height: 150px; } +.offlineIcon { + display: inline-block; + vertical-align: text-top; + margin-left: 2px; + width: 13px; + height: 13px; + background-repeat: no-repeat; + background-size: contain; + image-rendering: pixelated; +} + +.boardsBox table { + display: table; + margin-bottom: 10px; + overflow: hidden; + table-layout: fixed; + width: calc(100% + 4px); + margin-left: -2px; + border-collapse: separate; + border-spacing: 2px 0; +} + +.boardsBox table thead th { + background: #fca; + color: #800; + border: 1px solid #800; + padding: 4px 15px 5px 5px; + text-align: left; + white-space: nowrap; +} + +.boardsBox table tbody td { + margin: 0; + padding: 4px 15px 4px 4px; + text-align: left; +} + +.boardsBox table tbody tr:nth-of-type( even ) { + background: #ede2d4; +} + +.boardCell { + position: static; + margin: 0; + padding: 0; + box-sizing: border-box; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.boardCell a { + color: inherit; +} + .boxBar, .infoboxBar { padding-left: 0.5em; line-height: 2em; @@ -158,16 +213,6 @@ content: '✔ '; } -.list, .subplebbit a { - color: var(--homepage-box-link-text-color); - text-decoration: var(--homepage-box-link-text-decoration); -} - -.list a:hover { - color: var(--homepage-box-link-text-color-hover); - text-decoration: var(--homepage-box-link-text-decoration-hover); -} - .nsfw { color: red; font-weight: 700; @@ -175,47 +220,6 @@ font-size: smaller; } -.offlineIcon { - display: inline-block; - vertical-align: text-top; - margin-right: 2px; - width: 13px; - height: 13px; - background-repeat: no-repeat; - background-size: contain; - image-rendering: pixelated; -} - -.boardsBoxContent { - font-size: 93%; - padding: 0.5em; - padding-top: 0.25em; - padding-bottom: 0; - line-height: 130%; - display: table; -} - -.boardsBoxContent .subplebbit { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.boardsBoxContent .column { - float: left; - width: 168px; - margin-right: 15px; - word-break: break-word; -} - -.boardsBoxContent h3 { - font-size: 100%; - font-weight: 700; - text-decoration: underline; - text-transform: capitalize; - color: var(--homepage-box-bar-text-color); -} - .footer a { text-transform: capitalize; } diff --git a/src/views/home/home.tsx b/src/views/home/home.tsx index cedf401c..67db5cd6 100644 --- a/src/views/home/home.tsx +++ b/src/views/home/home.tsx @@ -7,7 +7,7 @@ import packageJson from '../../../package.json'; import useDefaultSubplebbits, { useDefaultSubplebbitAddresses } from '../../hooks/use-default-subplebbits'; import useSubplebbitsStats from '../../hooks/use-subplebbits-stats'; import PopularThreadsBox from './popular-threads-box'; -import BoardsBox from './boards-box'; +import BoardsList from './boards-list'; // https://github.com/plebbit/temporary-default-subplebbits/blob/master/README.md // plebchan shouldn't consider 'vulgar' or 'anti' as nsfw tags, unlike more sfw-oriented clients @@ -214,7 +214,7 @@ const Home = () => { - +