feat: add filters dropdown from topbar, for easy access on mobile

This commit is contained in:
Tom (plebeius.eth)
2025-03-23 17:42:12 +01:00
parent 7921ca523e
commit 787bb6b982
5 changed files with 249 additions and 125 deletions

View File

@@ -304,12 +304,8 @@ const HeaderTitle = ({ title, shortAddress, pendingPostSubplebbitAddress }: { ti
const subplebbitAddress = params.subplebbitAddress;
const contentOptionsStore = useContentOptionsStore();
const hasUnhiddenAnyNsfwCommunity =
!contentOptionsStore.hideAdultCommunities ||
!contentOptionsStore.hideGoreCommunities ||
!contentOptionsStore.hideAntiCommunities ||
!contentOptionsStore.hideVulgarCommunities;
const { hideAdultCommunities, hideGoreCommunities, hideAntiCommunities, hideVulgarCommunities } = useContentOptionsStore();
const hasUnhiddenAnyNsfwCommunity = !hideAdultCommunities || !hideGoreCommunities || !hideAntiCommunities || !hideVulgarCommunities;
const isBroadlyNsfwSubplebbit = useIsBroadlyNsfwSubplebbit(subplebbitAddress || '');
const subplebbitTitle = <Link to={`/p/${isInPendingPostView ? pendingPostSubplebbitAddress : subplebbitAddress}`}>{title || shortAddress}</Link>;

View File

@@ -15,13 +15,45 @@
.dropdown {
float: left;
cursor: pointer;
display: inline;
position: relative;
}
.subsDropdown {
padding-left: 5px;
cursor: pointer;
}
.filterDropdown .dropChoices {
padding: 0 4px 4px 4px;
text-transform: none;
font-size: 11px;
}
.filterDropdownItem label {
cursor: pointer;
padding-top: 2px;
}
.filterDropdownItem {
padding-top: 4px;
}
.filterDropdownItem input {
margin-right: 5px;
}
.nsfwFilters {
background-image: url("/assets/buttons/droparrowgray.gif");
background-repeat: no-repeat;
background-position: right bottom;
display: inline-block;
padding-right: 21px;
cursor: pointer;
height: 13px;
padding-top: 10px;
text-transform: uppercase;
font-size: 9px;
}
.dropChoices {

View File

@@ -1,4 +1,4 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { useEffect, useRef, useState } from 'react';
import { Link, useLocation, useNavigate, useParams } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { useAccount, useAccountSubplebbits } from '@plebbit/plebbit-react-hooks';
@@ -7,46 +7,25 @@ import styles from './topbar.module.css';
import { useDefaultSubplebbitAddresses } from '../../hooks/use-default-subplebbits';
import useTimeFilter from '../../hooks/use-time-filter';
import { isAllView, isHomeView, isModView, isSubplebbitView } from '../../lib/utils/view-utils';
import useContentOptionsStore from '../../stores/use-content-options-store';
const sortTypes = ['hot', 'new', 'active', 'controversialAll', 'topAll'];
const isElectron = window.isElectron === true;
const TopBar = () => {
const account = useAccount();
const subplebbitAddresses = useDefaultSubplebbitAddresses();
const FiltersDropdown = () => {
const { t } = useTranslation();
const location = useLocation();
const params = useParams();
const subscriptions = account?.subscriptions;
const isinAllView = isAllView(location.pathname);
const isInHomeView = isHomeView(location.pathname);
const isInModView = isModView(location.pathname);
const location = useLocation();
const navigate = useNavigate();
const isInSubplebbitView = isSubplebbitView(location.pathname, params);
const homeButtonClass = isInHomeView ? styles.selected : styles.choice;
const { accountSubplebbits } = useAccountSubplebbits();
const accountSubplebbitAddresses = Object.keys(accountSubplebbits);
const isinAllView = isAllView(location.pathname);
const isInModView = isModView(location.pathname);
const { timeFilterName, timeFilterNames } = useTimeFilter();
const selectedTimeFilter = timeFilterName || (isInSubplebbitView ? 'all' : timeFilterName);
const [isSubsDropdownOpen, setIsSubsDropdownOpen] = useState(false);
const toggleSubsDropdown = () => setIsSubsDropdownOpen(!isSubsDropdownOpen);
const subsDropdownRef = useRef<HTMLDivElement>(null);
const subsdropdownItemsRef = useRef<HTMLDivElement>(null);
const subsDropdownClass = isSubsDropdownOpen ? styles.visible : styles.hidden;
const [isSortsDropdownOpen, setIsSortsDropdownOpen] = useState(false);
const toggleSortsDropdown = () => setIsSortsDropdownOpen(!isSortsDropdownOpen);
const sortsDropdownRef = useRef<HTMLDivElement>(null);
const sortsdropdownItemsRef = useRef<HTMLDivElement>(null);
const sortsDropdownClass = isSortsDropdownOpen ? styles.visible : styles.hidden;
const [isFilterDropdownOpen, setIsFilterDropdownOpen] = useState(false);
const toggleFilterDropdown = () => setIsFilterDropdownOpen(!isFilterDropdownOpen);
const filterDropdownRef = useRef<HTMLDivElement>(null);
const filterdropdownItemsRef = useRef<HTMLDivElement>(null);
const filterDropdownClass = isFilterDropdownOpen ? styles.visible : styles.hidden;
const sortLabels = [t('hot'), t('new'), t('active'), t('controversial'), t('top')];
const selectedSortType = params.sortType || 'hot';
@@ -61,37 +40,12 @@ const TopBar = () => {
: `/${selectedSortType}/${timeFilterName}`;
};
const getSelectedSortLabel = () => {
const index = sortTypes.indexOf(selectedSortType);
return index >= 0 ? sortLabels[index] : sortLabels[0];
const handleClickOutside = (event: MouseEvent) => {
if (filterDropdownRef.current && !filterDropdownRef.current.contains(event.target as Node)) {
setIsFilterDropdownOpen(false);
}
};
const handleClickOutside = useCallback(
(event: MouseEvent) => {
const target = event.target as Node;
const isOutsideSubs =
subsDropdownRef.current && !subsDropdownRef.current.contains(target) && subsdropdownItemsRef.current && !subsdropdownItemsRef.current.contains(target);
const isOutsideSorts =
sortsDropdownRef.current && !sortsDropdownRef.current.contains(target) && sortsdropdownItemsRef.current && !sortsdropdownItemsRef.current.contains(target);
const isOutsideFilter =
filterDropdownRef.current && !filterDropdownRef.current.contains(target) && filterdropdownItemsRef.current && !filterdropdownItemsRef.current.contains(target);
if (isOutsideSubs) {
setIsSubsDropdownOpen(false);
}
if (isOutsideSorts) {
setIsSortsDropdownOpen(false);
}
if (isOutsideFilter) {
setIsFilterDropdownOpen(false);
}
},
[subsDropdownRef, subsdropdownItemsRef, sortsDropdownRef, sortsdropdownItemsRef, filterDropdownRef, filterdropdownItemsRef],
);
useEffect(() => {
document.addEventListener('mousedown', handleClickOutside);
return () => {
@@ -99,8 +53,155 @@ const TopBar = () => {
};
}, [handleClickOutside]);
const isConnectedToRpc = !!account?.plebbitOptions.plebbitRpcClientsOptions;
const {
blurNsfwThumbnails,
hideAdultCommunities,
hideGoreCommunities,
hideAntiCommunities,
hideVulgarCommunities,
setBlurNsfwThumbnails,
setHideAdultCommunities,
setHideGoreCommunities,
setHideAntiCommunities,
setHideVulgarCommunities,
} = useContentOptionsStore();
const [hideNsfwFilters, setHideNsfwFilters] = useState(true);
return (
<div className={`${styles.dropdown} ${styles.filterDropdown}`} ref={filterDropdownRef}>
<span className={styles.selectedTitle} onClick={() => setIsFilterDropdownOpen(!isFilterDropdownOpen)}>
{t('filters')}
</span>
{isFilterDropdownOpen && (
<div className={styles.dropChoices}>
<div className={styles.filterDropdownItem}>
posts from:{' '}
<select
onChange={(e) => {
navigate(getTimeFilterLink(e.target.value));
}}
value={selectedTimeFilter}
>
{timeFilterNames.map((timeFilterName, index) => (
<option value={timeFilterName} key={index}>
{timeFilterNames[index]}
</option>
))}
</select>
</div>
<div className={styles.filterDropdownItem}>
sort posts by:{' '}
<select
onChange={(e) => {
let dropdownLink = isInSubplebbitView ? `/p/${params.subplebbitAddress}/${e.target.value}` : isinAllView ? `/p/all/${e.target.value}` : e.target.value;
if (timeFilterName) {
dropdownLink += `/${timeFilterName}`;
}
navigate(dropdownLink);
}}
value={selectedSortType}
>
{sortTypes.map((sortType, index) => (
<option value={sortType} key={index}>
{sortLabels[index]}
</option>
))}
</select>
</div>
<div className={`${styles.filterDropdownItem} ${styles.nsfwFilters}`} onClick={() => setHideNsfwFilters(!hideNsfwFilters)}>
NSFW filters
</div>
{!hideNsfwFilters && (
<>
<div className={styles.filterDropdownItem}>
<label htmlFor='blurNSFWCheckbox'>
<input type='checkbox' id='blurNSFWCheckbox' checked={blurNsfwThumbnails} onChange={(e) => setBlurNsfwThumbnails(e.target.checked)} />
blur NSFW/18+ media
</label>
</div>
<div className={styles.filterDropdownItem}>
<label htmlFor='nsfwCheckbox'>
<input
type='checkbox'
id='nsfwCheckbox'
ref={(el) => {
if (el) {
const allChecked = !hideAdultCommunities && !hideGoreCommunities && !hideAntiCommunities && !hideVulgarCommunities;
const someChecked = !hideAdultCommunities || !hideGoreCommunities || !hideAntiCommunities || !hideVulgarCommunities;
el.checked = allChecked;
el.indeterminate = someChecked && !allChecked;
}
}}
onChange={(e) => {
const newValue = e.target.checked;
setHideAdultCommunities(!newValue);
setHideGoreCommunities(!newValue);
setHideAntiCommunities(!newValue);
setHideVulgarCommunities(!newValue);
}}
/>
include NSFW/18+ communities
</label>
</div>
<div className={styles.filterDropdownItem} style={{ paddingLeft: '20px' }}>
<label htmlFor='adultCommunitiesCheckbox'>
<input type='checkbox' id='adultCommunitiesCheckbox' checked={!hideAdultCommunities} onChange={(e) => setHideAdultCommunities(!e.target.checked)} />
adult communities
</label>
</div>
<div className={styles.filterDropdownItem} style={{ paddingLeft: '20px' }}>
<label htmlFor='goreCommunitiesCheckbox'>
<input type='checkbox' id='goreCommunitiesCheckbox' checked={!hideGoreCommunities} onChange={(e) => setHideGoreCommunities(!e.target.checked)} />
gore communities
</label>
</div>
<div className={styles.filterDropdownItem} style={{ paddingLeft: '20px' }}>
<label htmlFor='antiCommunitiesCheckbox'>
<input type='checkbox' id='antiCommunitiesCheckbox' checked={!hideAntiCommunities} onChange={(e) => setHideAntiCommunities(!e.target.checked)} />
anti communities
</label>
</div>
<div className={styles.filterDropdownItem} style={{ paddingLeft: '20px' }}>
<label htmlFor='vulgarCommunitiesCheckbox'>
<input type='checkbox' id='vulgarCommunitiesCheckbox' checked={!hideVulgarCommunities} onChange={(e) => setHideVulgarCommunities(!e.target.checked)} />
vulgar communities
</label>
</div>
</>
)}
</div>
)}
</div>
);
};
const CommunitiesDropdown = () => {
const { t } = useTranslation();
const account = useAccount();
const navigate = useNavigate();
const subscriptions = account?.subscriptions;
const isConnectedToRpc = !!account?.plebbitOptions.plebbitRpcClientsOptions;
const [isSubsDropdownOpen, setIsSubsDropdownOpen] = useState(false);
const toggleSubsDropdown = () => setIsSubsDropdownOpen(!isSubsDropdownOpen);
const subsDropdownRef = useRef<HTMLDivElement>(null);
const subsdropdownItemsRef = useRef<HTMLDivElement>(null);
const subsDropdownClass = isSubsDropdownOpen ? styles.visible : styles.hidden;
const handleClickOutside = (event: MouseEvent) => {
if (subsDropdownRef.current && !subsDropdownRef.current.contains(event.target as Node)) {
setIsSubsDropdownOpen(false);
}
};
useEffect(() => {
if (isSubsDropdownOpen) {
document.addEventListener('mousedown', handleClickOutside);
}
}, [isSubsDropdownOpen]);
const handleCreateCommunity = () => {
// creating a community only works if the user is running a full node
if (isElectron || isConnectedToRpc) {
@@ -116,54 +217,47 @@ const TopBar = () => {
}
};
return (
<div className={`${styles.dropdown} ${styles.subsDropdown}`} ref={subsDropdownRef} onClick={toggleSubsDropdown}>
<span className={styles.selectedTitle}>{t('my_communities')}</span>
<div className={`${styles.dropChoices} ${styles.subsDropChoices} ${subsDropdownClass}`} ref={subsdropdownItemsRef}>
{subscriptions?.map((subscription: string, index: number) => (
<Link key={index} to={`/p/${subscription}`} className={styles.dropdownItem}>
{Plebbit.getShortAddress(subscription)}
</Link>
))}
<span onClick={handleCreateCommunity} className={`${styles.dropdownItem} ${styles.myCommunitiesItemButtonDotted}`}>
{t('create_community')}
</span>
<Link to='/communities/vote' className={`${styles.dropdownItem} ${styles.myCommunitiesItemButton}`}>
{t('default_communities')}
</Link>
<Link to='/communities/subscriber' className={`${styles.dropdownItem} ${styles.myCommunitiesItemButton}`}>
{t('edit_subscriptions')}
</Link>
</div>
</div>
);
};
const TopBar = () => {
const subplebbitAddresses = useDefaultSubplebbitAddresses();
const { t } = useTranslation();
const location = useLocation();
const params = useParams();
const isinAllView = isAllView(location.pathname);
const isInHomeView = isHomeView(location.pathname);
const isInModView = isModView(location.pathname);
const homeButtonClass = isInHomeView ? styles.selected : styles.choice;
const { accountSubplebbits } = useAccountSubplebbits();
const accountSubplebbitAddresses = Object.keys(accountSubplebbits);
return (
<div className={styles.headerArea}>
<div className={styles.widthClip}>
<div className={`${styles.dropdown} ${styles.subsDropdown}`} ref={subsDropdownRef} onClick={toggleSubsDropdown}>
<span className={styles.selectedTitle}>{t('my_communities')}</span>
<div className={`${styles.dropChoices} ${styles.subsDropChoices} ${subsDropdownClass}`} ref={subsdropdownItemsRef}>
{subscriptions?.map((subscription: string, index: number) => (
<Link key={index} to={`/p/${subscription}`} className={styles.dropdownItem}>
{Plebbit.getShortAddress(subscription)}
</Link>
))}
<span onClick={handleCreateCommunity} className={`${styles.dropdownItem} ${styles.myCommunitiesItemButtonDotted}`}>
{t('create_community')}
</span>
<Link to='/communities/vote' className={`${styles.dropdownItem} ${styles.myCommunitiesItemButton}`}>
{t('default_communities')}
</Link>
<Link to='/communities/subscriber' className={`${styles.dropdownItem} ${styles.myCommunitiesItemButton}`}>
{t('edit_subscriptions')}
</Link>
</div>
</div>
<div className={styles.dropdown} ref={sortsDropdownRef} onClick={toggleSortsDropdown}>
<span className={styles.selectedTitle}>{getSelectedSortLabel()}</span>
<div className={`${styles.dropChoices} ${styles.sortsDropChoices} ${sortsDropdownClass}`} ref={sortsdropdownItemsRef}>
{sortTypes.map((sortType, index) => {
let dropdownLink = isInSubplebbitView ? `/p/${params.subplebbitAddress}/${sortType}` : isinAllView ? `/p/all/${sortType}` : sortType;
if (timeFilterName) {
dropdownLink += `/${timeFilterName}`;
}
return (
<Link to={dropdownLink} key={index} className={styles.dropdownItem}>
{sortLabels[index]}
</Link>
);
})}
</div>
</div>
<div className={styles.dropdown} ref={filterDropdownRef} onClick={toggleFilterDropdown}>
<span className={styles.selectedTitle}>{selectedTimeFilter}</span>
<div className={`${styles.dropChoices} ${styles.filterDropChoices} ${filterDropdownClass}`} ref={filterdropdownItemsRef}>
{timeFilterNames.slice(0, -1).map((timeFilterName, index) => (
<Link to={getTimeFilterLink(timeFilterName)} key={index} className={styles.dropdownItem}>
{timeFilterNames[index]}
</Link>
))}
</div>
</div>
<CommunitiesDropdown />
<FiltersDropdown />
<div className={styles.srList}>
<ul className={styles.srBar}>
<li>

View File

@@ -149,17 +149,18 @@ const ContentOptions = () => {
<br />
<br />
<div className={styles.filterSettingTitle}>{t('communities')}</div>
<input type='checkbox' id='hideAdultCommunities' checked={hideAdultCommunities} onChange={(e) => setHideAdultCommunities(e.target.checked)} />
<label htmlFor='hideAdultCommunities'>{t('hide_adult')} (NSFW/18+)</label>
<br />
<input type='checkbox' id='hideAntiCommunities' checked={hideAntiCommunities} onChange={(e) => setHideAntiCommunities(e.target.checked)} />
<label htmlFor='hideAntiCommunities'>{t('hide_anti')}</label>
<br />
<input type='checkbox' id='hideGoreCommunities' checked={hideGoreCommunities} onChange={(e) => setHideGoreCommunities(e.target.checked)} />
<label htmlFor='hideGoreCommunities'>{t('hide_gore')} (NSFW/18+)</label>
<br />
<input type='checkbox' id='hideVulgarCommunities' checked={hideVulgarCommunities} onChange={(e) => setHideVulgarCommunities(e.target.checked)} />
<label htmlFor='hideVulgarCommunities'>{t('hide_vulgar')}</label>
<input
type='checkbox'
id='hideAdultCommunities'
checked={hideAdultCommunities || hideAntiCommunities || hideGoreCommunities || hideVulgarCommunities}
onChange={(e) => {
setHideAdultCommunities(e.target.checked);
setHideAntiCommunities(e.target.checked);
setHideGoreCommunities(e.target.checked);
setHideVulgarCommunities(e.target.checked);
}}
/>
<label htmlFor='hideAdultCommunities'>Hide communities tagged as NSFW/18+</label>
</div>
);
};
@@ -210,7 +211,7 @@ const GeneralSettings = () => {
<span className={styles.categoryTitle}>{t('version')}</span>
<span className={styles.categorySettings}>
<div className={styles.version}>
seedit <Version />
<Version />
{isElectron && (
<a className={styles.fullNodeStats} href='http://localhost:50019/webui/' target='_blank' rel='noreferrer'>
{t('node_stats')}

View File

@@ -261,7 +261,8 @@ const Subplebbit = () => {
}, [feed, setPinnedPostsCount]);
// over 18 warning for subplebbit with nsfw tag in multisub default list
const { hasAcceptedWarning } = useContentOptionsStore();
const { hideAdultCommunities, hideGoreCommunities, hideAntiCommunities, hideVulgarCommunities } = useContentOptionsStore();
const hasUnhiddenAnyNsfwCommunity = !hideAdultCommunities || !hideGoreCommunities || !hideAntiCommunities || !hideVulgarCommunities;
const isBroadlyNsfwSubplebbit = useIsBroadlyNsfwSubplebbit(subplebbitAddress || '');
// page title
@@ -269,7 +270,7 @@ const Subplebbit = () => {
document.title = title ? title : shortAddress || subplebbitAddress;
}, [title, shortAddress, subplebbitAddress]);
return isBroadlyNsfwSubplebbit && !hasAcceptedWarning ? (
return isBroadlyNsfwSubplebbit && !hasUnhiddenAnyNsfwCommunity ? (
<Over18Warning />
) : (
<div className={styles.content}>