Compare commits
3 Commits
1928c0e4e9
...
25fcdce7b0
Author | SHA1 | Date | |
---|---|---|---|
|
25fcdce7b0 | ||
|
b48b4478af | ||
|
b6a8fc435d |
155
src/components/ModsFilter.tsx
Normal file
155
src/components/ModsFilter.tsx
Normal file
@ -0,0 +1,155 @@
|
||||
import { useAppSelector } from 'hooks'
|
||||
import React from 'react'
|
||||
import { Dispatch, SetStateAction } from 'react'
|
||||
import { FilterOptions, ModeratedFilter, NSFWFilter, SortBy } from 'types'
|
||||
|
||||
type Props = {
|
||||
filterOptions: FilterOptions
|
||||
setFilterOptions: Dispatch<SetStateAction<FilterOptions>>
|
||||
}
|
||||
|
||||
export const ModFilter = React.memo(
|
||||
({ filterOptions, setFilterOptions }: Props) => {
|
||||
const userState = useAppSelector((state) => state.user)
|
||||
|
||||
return (
|
||||
<div className='IBMSecMain'>
|
||||
<div className='FiltersMain'>
|
||||
<div className='FiltersMainElement'>
|
||||
<div className='dropdown dropdownMain'>
|
||||
<button
|
||||
className='btn dropdown-toggle btnMain btnMainDropdown'
|
||||
aria-expanded='false'
|
||||
data-bs-toggle='dropdown'
|
||||
type='button'
|
||||
>
|
||||
{filterOptions.sort}
|
||||
</button>
|
||||
|
||||
<div className='dropdown-menu dropdownMainMenu'>
|
||||
{Object.values(SortBy).map((item, index) => (
|
||||
<div
|
||||
key={`sortByItem-${index}`}
|
||||
className='dropdown-item dropdownMainMenuItem'
|
||||
onClick={() =>
|
||||
setFilterOptions((prev) => ({
|
||||
...prev,
|
||||
sort: item
|
||||
}))
|
||||
}
|
||||
>
|
||||
{item}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='FiltersMainElement'>
|
||||
<div className='dropdown dropdownMain'>
|
||||
<button
|
||||
className='btn dropdown-toggle btnMain btnMainDropdown'
|
||||
aria-expanded='false'
|
||||
data-bs-toggle='dropdown'
|
||||
type='button'
|
||||
>
|
||||
{filterOptions.moderated}
|
||||
</button>
|
||||
<div className='dropdown-menu dropdownMainMenu'>
|
||||
{Object.values(ModeratedFilter).map((item, index) => {
|
||||
if (item === ModeratedFilter.Unmoderated_Fully) {
|
||||
const isAdmin =
|
||||
userState.user?.npub ===
|
||||
import.meta.env.VITE_REPORTING_NPUB
|
||||
|
||||
if (!isAdmin) return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
key={`moderatedFilterItem-${index}`}
|
||||
className='dropdown-item dropdownMainMenuItem'
|
||||
onClick={() =>
|
||||
setFilterOptions((prev) => ({
|
||||
...prev,
|
||||
moderated: item
|
||||
}))
|
||||
}
|
||||
>
|
||||
{item}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='FiltersMainElement'>
|
||||
<div className='dropdown dropdownMain'>
|
||||
<button
|
||||
className='btn dropdown-toggle btnMain btnMainDropdown'
|
||||
aria-expanded='false'
|
||||
data-bs-toggle='dropdown'
|
||||
type='button'
|
||||
>
|
||||
{filterOptions.nsfw}
|
||||
</button>
|
||||
<div className='dropdown-menu dropdownMainMenu'>
|
||||
{Object.values(NSFWFilter).map((item, index) => (
|
||||
<div
|
||||
key={`nsfwFilterItem-${index}`}
|
||||
className='dropdown-item dropdownMainMenuItem'
|
||||
onClick={() =>
|
||||
setFilterOptions((prev) => ({
|
||||
...prev,
|
||||
nsfw: item
|
||||
}))
|
||||
}
|
||||
>
|
||||
{item}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='FiltersMainElement'>
|
||||
<div className='dropdown dropdownMain'>
|
||||
<button
|
||||
className='btn dropdown-toggle btnMain btnMainDropdown'
|
||||
aria-expanded='false'
|
||||
data-bs-toggle='dropdown'
|
||||
type='button'
|
||||
>
|
||||
{filterOptions.source === window.location.host
|
||||
? `Show From: ${filterOptions.source}`
|
||||
: 'Show All'}
|
||||
</button>
|
||||
<div className='dropdown-menu dropdownMainMenu'>
|
||||
<div
|
||||
className='dropdown-item dropdownMainMenuItem'
|
||||
onClick={() =>
|
||||
setFilterOptions((prev) => ({
|
||||
...prev,
|
||||
source: window.location.host
|
||||
}))
|
||||
}
|
||||
>
|
||||
Show From: {window.location.host}
|
||||
</div>
|
||||
<div
|
||||
className='dropdown-item dropdownMainMenuItem'
|
||||
onClick={() =>
|
||||
setFilterOptions((prev) => ({
|
||||
...prev,
|
||||
source: 'Show All'
|
||||
}))
|
||||
}
|
||||
>
|
||||
Show All
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
@ -6,7 +6,8 @@ export const LANDING_PAGE_DATA = {
|
||||
'naddr1qvzqqqrkcgpzquuz5nxzzap2c034s8cuv5ayr7gjaxz7d22pgwfh0qpmsesy9eflqp4nxvp5xqer5den8qexzdrrvverzde5xfskxvm9xv6nsvtxx93nvdfnvy6rze3exyex2wfcx4jnvcfexscngveexvmnwwpsxd3rsd3kxq6ryef4xdnr5vpcxs6nwwp3x5knyd3evckngetxxcknjdfkx5kngdfhvgukvwfjxsunseqnend73',
|
||||
'naddr1qvzqqqrkcgpzquuz5nxzzap2c034s8cuv5ayr7gjaxz7d22pgwfh0qpmsesy9eflqp4nxvp5xqer5den8qexzdrrvverzde5xfskxvm9xv6nsvtxx93nvdfnvy6rze3exyex2wfcx4jnvcfexscngveexvmnwwpsxd3rsd3kxq6ryef4xdnr5d3excenzvf5xgkkvdny8qkngveex5knjcnxxqkn2efnx3jrxvpcxgukxdggsmal6',
|
||||
'naddr1qvzqqqrkcgpzquuz5nxzzap2c034s8cuv5ayr7gjaxz7d22pgwfh0qpmsesy9eflqp4nxvp5xqer5den8qexzdrrvverzde5xfskxvm9xv6nsvtxx93nvdfnvy6rze3exyex2wfcx4jnvcfexscngveexvmnwwpsxd3rsd3kxq6ryef4xdnr5dp4xsex2e3cxuknsdryvvkngc3sxcknjef4vcknvvmyvcukyd3kvd3rxdgnuver5',
|
||||
'naddr1qvzqqqrkcgpzquuz5nxzzap2c034s8cuv5ayr7gjaxz7d22pgwfh0qpmsesy9eflqp4nxvp5xqer5den8qexzdrrvverzde5xfskxvm9xv6nsvtxx93nvdfnvy6rze3exyex2wfcx4jnvcfexscngveexvmnwwpsxd3rsd3kxq6ryef4xdnr5vf5x9nrxcekxvknjvmzxvkngcfsx5kkzcf3xqknsvmrvgenwe3j8p3nzwgka59vj'
|
||||
'naddr1qvzqqqrkcgpzquuz5nxzzap2c034s8cuv5ayr7gjaxz7d22pgwfh0qpmsesy9eflqp4nxvp5xqer5den8qexzdrrvverzde5xfskxvm9xv6nsvtxx93nvdfnvy6rze3exyex2wfcx4jnvcfexscngveexvmnwwpsxd3rsd3kxq6ryef4xdnr5vf5x9nrxcekxvknjvmzxvkngcfsx5kkzcf3xqknsvmrvgenwe3j8p3nzwgka59vj',
|
||||
'naddr1qvzqqqrkcgpzph2jv2ejvdk27hn36dt57j6f69f5h0zccve3xceujq5z9jk8ym8wqp4nxvp5xqer5eryx5ervvnzxvervvekvdskvdt9xuckgve4xu6xvdrzxsukgvf4xv6xycnrx5uxxvenxvcnxd3nxd3njvpj8qerycmpvvmnydnrv4jn5wrpv5mrvwpsxgknxwp4xqkngetpxqknjd35xcknsv3cv9jnxvtp8ycxyegq5ndhc'
|
||||
],
|
||||
awesomeMods: [
|
||||
'naddr1qvzqqqrkcgpzquuz5nxzzap2c034s8cuv5ayr7gjaxz7d22pgwfh0qpmsesy9eflqp4nxvp5xqer5den8qexzdrrvverzde5xfskxvm9xv6nsvtxx93nvdfnvy6rze3exyex2wfcx4jnvcfexscngveexvmnwwpsxd3rsd3kxq6ryef4xdnr5d3excenzvf5xgkkvdny8qkngveex5knjcnxxqkn2efnx3jrxvpcxgukxdggsmal6',
|
||||
@ -18,7 +19,7 @@ export const LANDING_PAGE_DATA = {
|
||||
"Baldur's Gate 3",
|
||||
'Cyberpunk 2077',
|
||||
'ELDEN RING',
|
||||
'FINAL FANTASY VII REMAKE INTERGRADE'
|
||||
'The Coffin of Andy and Leyley'
|
||||
]
|
||||
}
|
||||
// we use this object to check if a user has reacted positively or negatively to a post
|
||||
|
@ -1,5 +1,6 @@
|
||||
export * from './redux'
|
||||
export * from './useDidMount'
|
||||
export * from './useFilteredMods'
|
||||
export * from './useGames'
|
||||
export * from './useMuteLists'
|
||||
export * from './useNSFWList'
|
||||
|
77
src/hooks/useFilteredMods.ts
Normal file
77
src/hooks/useFilteredMods.ts
Normal file
@ -0,0 +1,77 @@
|
||||
import { useMemo } from 'react'
|
||||
import { IUserState } from 'store/reducers/user'
|
||||
import {
|
||||
FilterOptions,
|
||||
ModDetails,
|
||||
ModeratedFilter,
|
||||
MuteLists,
|
||||
NSFWFilter,
|
||||
SortBy
|
||||
} from 'types'
|
||||
|
||||
export const useFilteredMods = (
|
||||
mods: ModDetails[],
|
||||
userState: IUserState,
|
||||
filterOptions: FilterOptions,
|
||||
nsfwList: string[],
|
||||
muteLists: {
|
||||
admin: MuteLists
|
||||
user: MuteLists
|
||||
}
|
||||
) => {
|
||||
return useMemo(() => {
|
||||
const nsfwFilter = (mods: ModDetails[]) => {
|
||||
// Determine the filtering logic based on the NSFW filter option
|
||||
switch (filterOptions.nsfw) {
|
||||
case NSFWFilter.Hide_NSFW:
|
||||
// If 'Hide_NSFW' is selected, filter out NSFW mods
|
||||
return mods.filter((mod) => !mod.nsfw && !nsfwList.includes(mod.aTag))
|
||||
case NSFWFilter.Show_NSFW:
|
||||
// If 'Show_NSFW' is selected, return all mods (no filtering)
|
||||
return mods
|
||||
case NSFWFilter.Only_NSFW:
|
||||
// If 'Only_NSFW' is selected, filter to show only NSFW mods
|
||||
return mods.filter((mod) => mod.nsfw || nsfwList.includes(mod.aTag))
|
||||
}
|
||||
}
|
||||
|
||||
let filtered = nsfwFilter(mods)
|
||||
|
||||
const isAdmin = userState.user?.npub === import.meta.env.VITE_REPORTING_NPUB
|
||||
const isUnmoderatedFully =
|
||||
filterOptions.moderated === ModeratedFilter.Unmoderated_Fully
|
||||
|
||||
// Only apply filtering if the user is not an admin or the admin has not selected "Unmoderated Fully"
|
||||
if (!(isAdmin && isUnmoderatedFully)) {
|
||||
filtered = filtered.filter(
|
||||
(mod) =>
|
||||
!muteLists.admin.authors.includes(mod.author) &&
|
||||
!muteLists.admin.replaceableEvents.includes(mod.aTag)
|
||||
)
|
||||
}
|
||||
|
||||
if (filterOptions.moderated === ModeratedFilter.Moderated) {
|
||||
filtered = filtered.filter(
|
||||
(mod) =>
|
||||
!muteLists.user.authors.includes(mod.author) &&
|
||||
!muteLists.user.replaceableEvents.includes(mod.aTag)
|
||||
)
|
||||
}
|
||||
|
||||
if (filterOptions.sort === SortBy.Latest) {
|
||||
filtered.sort((a, b) => b.published_at - a.published_at)
|
||||
} else if (filterOptions.sort === SortBy.Oldest) {
|
||||
filtered.sort((a, b) => a.published_at - b.published_at)
|
||||
}
|
||||
|
||||
return filtered
|
||||
}, [
|
||||
userState.user?.npub,
|
||||
filterOptions.sort,
|
||||
filterOptions.moderated,
|
||||
filterOptions.nsfw,
|
||||
mods,
|
||||
muteLists,
|
||||
nsfwList
|
||||
])
|
||||
}
|
@ -1,50 +1,40 @@
|
||||
import { LoadingSpinner } from 'components/LoadingSpinner'
|
||||
import { ModCard } from 'components/ModCard'
|
||||
import { ModFilter } from 'components/ModsFilter'
|
||||
import { PaginationWithPageNumbers } from 'components/Pagination'
|
||||
import { MAX_MODS_PER_PAGE, T_TAG_VALUE } from 'constants.ts'
|
||||
import { RelayController } from 'controllers'
|
||||
import { useAppSelector, useMuteLists } from 'hooks'
|
||||
import {
|
||||
useAppSelector,
|
||||
useFilteredMods,
|
||||
useMuteLists,
|
||||
useNSFWList
|
||||
} from 'hooks'
|
||||
import { Filter, kinds } from 'nostr-tools'
|
||||
import { Subscription } from 'nostr-tools/abstract-relay'
|
||||
import React, {
|
||||
Dispatch,
|
||||
SetStateAction,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState
|
||||
} from 'react'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import { toast } from 'react-toastify'
|
||||
import { ModDetails } from 'types'
|
||||
import {
|
||||
FilterOptions,
|
||||
ModDetails,
|
||||
ModeratedFilter,
|
||||
NSFWFilter,
|
||||
SortBy
|
||||
} from 'types'
|
||||
import { extractModData, isModDataComplete, log, LogType } from 'utils'
|
||||
|
||||
enum SortByEnum {
|
||||
Latest = 'Latest',
|
||||
Oldest = 'Oldest',
|
||||
Best_Rated = 'Best Rated',
|
||||
Worst_Rated = 'Worst Rated'
|
||||
}
|
||||
|
||||
enum ModeratedFilterEnum {
|
||||
Moderated = 'Moderated',
|
||||
Unmoderated = 'Unmoderated',
|
||||
Unmoderated_Fully = 'Unmoderated Fully'
|
||||
}
|
||||
|
||||
interface FilterOptions {
|
||||
sort: SortByEnum
|
||||
moderated: ModeratedFilterEnum
|
||||
}
|
||||
|
||||
export const GamePage = () => {
|
||||
const params = useParams()
|
||||
const { name: gameName } = params
|
||||
const muteLists = useMuteLists()
|
||||
const nsfwList = useNSFWList()
|
||||
|
||||
const [filterOptions, setFilterOptions] = useState<FilterOptions>({
|
||||
sort: SortByEnum.Latest,
|
||||
moderated: ModeratedFilterEnum.Moderated
|
||||
sort: SortBy.Latest,
|
||||
nsfw: NSFWFilter.Hide_NSFW,
|
||||
source: window.location.host,
|
||||
moderated: ModeratedFilter.Moderated
|
||||
})
|
||||
const [mods, setMods] = useState<ModDetails[]>([])
|
||||
|
||||
@ -54,43 +44,13 @@ export const GamePage = () => {
|
||||
|
||||
const userState = useAppSelector((state) => state.user)
|
||||
|
||||
const filteredMods = useMemo(() => {
|
||||
let filtered: ModDetails[] = [...mods]
|
||||
const isAdmin = userState.user?.npub === import.meta.env.VITE_REPORTING_NPUB
|
||||
const isUnmoderatedFully =
|
||||
filterOptions.moderated === ModeratedFilterEnum.Unmoderated_Fully
|
||||
|
||||
// Only apply filtering if the user is not an admin or the admin has not selected "Unmoderated Fully"
|
||||
if (!(isAdmin && isUnmoderatedFully)) {
|
||||
filtered = filtered.filter(
|
||||
(mod) =>
|
||||
!muteLists.admin.authors.includes(mod.author) &&
|
||||
!muteLists.admin.replaceableEvents.includes(mod.aTag)
|
||||
)
|
||||
}
|
||||
|
||||
if (filterOptions.moderated === ModeratedFilterEnum.Moderated) {
|
||||
filtered = filtered.filter(
|
||||
(mod) =>
|
||||
!muteLists.user.authors.includes(mod.author) &&
|
||||
!muteLists.user.replaceableEvents.includes(mod.aTag)
|
||||
)
|
||||
}
|
||||
|
||||
if (filterOptions.sort === SortByEnum.Latest) {
|
||||
filtered.sort((a, b) => b.published_at - a.published_at)
|
||||
} else if (filterOptions.sort === SortByEnum.Oldest) {
|
||||
filtered.sort((a, b) => a.published_at - b.published_at)
|
||||
}
|
||||
|
||||
return filtered
|
||||
}, [
|
||||
const filteredMods = useFilteredMods(
|
||||
mods,
|
||||
userState.user?.npub,
|
||||
filterOptions.sort,
|
||||
filterOptions.moderated,
|
||||
userState,
|
||||
filterOptions,
|
||||
nsfwList,
|
||||
muteLists
|
||||
])
|
||||
)
|
||||
|
||||
// Pagination logic
|
||||
const totalGames = filteredMods.length
|
||||
@ -172,7 +132,7 @@ export const GamePage = () => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Filters
|
||||
<ModFilter
|
||||
filterOptions={filterOptions}
|
||||
setFilterOptions={setFilterOptions}
|
||||
/>
|
||||
@ -194,88 +154,3 @@ export const GamePage = () => {
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
type FiltersProps = {
|
||||
filterOptions: FilterOptions
|
||||
setFilterOptions: Dispatch<SetStateAction<FilterOptions>>
|
||||
}
|
||||
|
||||
const Filters = React.memo(
|
||||
({ filterOptions, setFilterOptions }: FiltersProps) => {
|
||||
const userState = useAppSelector((state) => state.user)
|
||||
|
||||
return (
|
||||
<div className='IBMSecMain'>
|
||||
<div className='FiltersMain'>
|
||||
<div className='FiltersMainElement'>
|
||||
<div className='dropdown dropdownMain'>
|
||||
<button
|
||||
className='btn dropdown-toggle btnMain btnMainDropdown'
|
||||
aria-expanded='false'
|
||||
data-bs-toggle='dropdown'
|
||||
type='button'
|
||||
>
|
||||
{filterOptions.sort}
|
||||
</button>
|
||||
|
||||
<div className='dropdown-menu dropdownMainMenu'>
|
||||
{Object.values(SortByEnum).map((item, index) => (
|
||||
<div
|
||||
key={`sortByItem-${index}`}
|
||||
className='dropdown-item dropdownMainMenuItem'
|
||||
onClick={() =>
|
||||
setFilterOptions((prev) => ({
|
||||
...prev,
|
||||
sort: item
|
||||
}))
|
||||
}
|
||||
>
|
||||
{item}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='FiltersMainElement'>
|
||||
<div className='dropdown dropdownMain'>
|
||||
<button
|
||||
className='btn dropdown-toggle btnMain btnMainDropdown'
|
||||
aria-expanded='false'
|
||||
data-bs-toggle='dropdown'
|
||||
type='button'
|
||||
>
|
||||
{filterOptions.moderated}
|
||||
</button>
|
||||
<div className='dropdown-menu dropdownMainMenu'>
|
||||
{Object.values(ModeratedFilterEnum).map((item, index) => {
|
||||
if (item === ModeratedFilterEnum.Unmoderated_Fully) {
|
||||
const isAdmin =
|
||||
userState.user?.npub ===
|
||||
import.meta.env.VITE_REPORTING_NPUB
|
||||
|
||||
if (!isAdmin) return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
key={`moderatedFilterItem-${index}`}
|
||||
className='dropdown-item dropdownMainMenuItem'
|
||||
onClick={() =>
|
||||
setFilterOptions((prev) => ({
|
||||
...prev,
|
||||
moderated: item
|
||||
}))
|
||||
}
|
||||
>
|
||||
{item}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
@ -1,52 +1,30 @@
|
||||
import { ModFilter } from 'components/ModsFilter'
|
||||
import { Pagination } from 'components/Pagination'
|
||||
import React, {
|
||||
Dispatch,
|
||||
SetStateAction,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState
|
||||
} from 'react'
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { createSearchParams, useNavigate } from 'react-router-dom'
|
||||
import { LoadingSpinner } from '../components/LoadingSpinner'
|
||||
import { ModCard } from '../components/ModCard'
|
||||
import { MOD_FILTER_LIMIT } from '../constants'
|
||||
import { useAppSelector, useMuteLists, useNSFWList } from '../hooks'
|
||||
import {
|
||||
useAppSelector,
|
||||
useFilteredMods,
|
||||
useMuteLists,
|
||||
useNSFWList
|
||||
} from '../hooks'
|
||||
import { appRoutes } from '../routes'
|
||||
import '../styles/filters.css'
|
||||
import '../styles/pagination.css'
|
||||
import '../styles/search.css'
|
||||
import '../styles/styles.css'
|
||||
import { ModDetails } from '../types'
|
||||
import {
|
||||
FilterOptions,
|
||||
ModDetails,
|
||||
ModeratedFilter,
|
||||
NSFWFilter,
|
||||
SortBy
|
||||
} from '../types'
|
||||
import { fetchMods } from '../utils'
|
||||
|
||||
enum SortBy {
|
||||
Latest = 'Latest',
|
||||
Oldest = 'Oldest',
|
||||
Best_Rated = 'Best Rated',
|
||||
Worst_Rated = 'Worst Rated'
|
||||
}
|
||||
|
||||
enum NSFWFilter {
|
||||
Hide_NSFW = 'Hide NSFW',
|
||||
Show_NSFW = 'Show NSFW',
|
||||
Only_NSFW = 'Only NSFW'
|
||||
}
|
||||
|
||||
enum ModeratedFilter {
|
||||
Moderated = 'Moderated',
|
||||
Unmoderated = 'Unmoderated',
|
||||
Unmoderated_Fully = 'Unmoderated Fully'
|
||||
}
|
||||
|
||||
interface FilterOptions {
|
||||
sort: SortBy
|
||||
nsfw: NSFWFilter
|
||||
source: string
|
||||
moderated: ModeratedFilter
|
||||
}
|
||||
|
||||
export const ModsPage = () => {
|
||||
const [isFetching, setIsFetching] = useState(false)
|
||||
const [mods, setMods] = useState<ModDetails[]>([])
|
||||
@ -111,61 +89,13 @@ export const ModsPage = () => {
|
||||
})
|
||||
}, [filterOptions.source, mods])
|
||||
|
||||
const filteredModList = useMemo(() => {
|
||||
const nsfwFilter = (mods: ModDetails[]) => {
|
||||
// Determine the filtering logic based on the NSFW filter option
|
||||
switch (filterOptions.nsfw) {
|
||||
case NSFWFilter.Hide_NSFW:
|
||||
// If 'Hide_NSFW' is selected, filter out NSFW mods
|
||||
return mods.filter((mod) => !mod.nsfw && !nsfwList.includes(mod.aTag))
|
||||
case NSFWFilter.Show_NSFW:
|
||||
// If 'Show_NSFW' is selected, return all mods (no filtering)
|
||||
return mods
|
||||
case NSFWFilter.Only_NSFW:
|
||||
// If 'Only_NSFW' is selected, filter to show only NSFW mods
|
||||
return mods.filter((mod) => mod.nsfw || nsfwList.includes(mod.aTag))
|
||||
}
|
||||
}
|
||||
|
||||
let filtered = nsfwFilter(mods)
|
||||
|
||||
const isAdmin = userState.user?.npub === import.meta.env.VITE_REPORTING_NPUB
|
||||
const isUnmoderatedFully =
|
||||
filterOptions.moderated === ModeratedFilter.Unmoderated_Fully
|
||||
|
||||
// Only apply filtering if the user is not an admin or the admin has not selected "Unmoderated Fully"
|
||||
if (!(isAdmin && isUnmoderatedFully)) {
|
||||
filtered = filtered.filter(
|
||||
(mod) =>
|
||||
!muteLists.admin.authors.includes(mod.author) &&
|
||||
!muteLists.admin.replaceableEvents.includes(mod.aTag)
|
||||
)
|
||||
}
|
||||
|
||||
if (filterOptions.moderated === ModeratedFilter.Moderated) {
|
||||
filtered = filtered.filter(
|
||||
(mod) =>
|
||||
!muteLists.user.authors.includes(mod.author) &&
|
||||
!muteLists.user.replaceableEvents.includes(mod.aTag)
|
||||
)
|
||||
}
|
||||
|
||||
if (filterOptions.sort === SortBy.Latest) {
|
||||
filtered.sort((a, b) => b.published_at - a.published_at)
|
||||
} else if (filterOptions.sort === SortBy.Oldest) {
|
||||
filtered.sort((a, b) => a.published_at - b.published_at)
|
||||
}
|
||||
|
||||
return filtered
|
||||
}, [
|
||||
userState.user?.npub,
|
||||
filterOptions.sort,
|
||||
filterOptions.moderated,
|
||||
filterOptions.nsfw,
|
||||
const filteredModList = useFilteredMods(
|
||||
mods,
|
||||
muteLists,
|
||||
nsfwList
|
||||
])
|
||||
userState,
|
||||
filterOptions,
|
||||
nsfwList,
|
||||
muteLists
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -174,7 +104,7 @@ export const ModsPage = () => {
|
||||
<div className='ContainerMain'>
|
||||
<div className='IBMSecMainGroup IBMSecMainGroupAlt'>
|
||||
<PageTitleRow />
|
||||
<Filters
|
||||
<ModFilter
|
||||
filterOptions={filterOptions}
|
||||
setFilterOptions={setFilterOptions}
|
||||
/>
|
||||
@ -261,154 +191,3 @@ const PageTitleRow = React.memo(() => {
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
type FiltersProps = {
|
||||
filterOptions: FilterOptions
|
||||
setFilterOptions: Dispatch<SetStateAction<FilterOptions>>
|
||||
}
|
||||
|
||||
const Filters = React.memo(
|
||||
({ filterOptions, setFilterOptions }: FiltersProps) => {
|
||||
const userState = useAppSelector((state) => state.user)
|
||||
|
||||
return (
|
||||
<div className='IBMSecMain'>
|
||||
<div className='FiltersMain'>
|
||||
<div className='FiltersMainElement'>
|
||||
<div className='dropdown dropdownMain'>
|
||||
<button
|
||||
className='btn dropdown-toggle btnMain btnMainDropdown'
|
||||
aria-expanded='false'
|
||||
data-bs-toggle='dropdown'
|
||||
type='button'
|
||||
>
|
||||
{filterOptions.sort}
|
||||
</button>
|
||||
|
||||
<div className='dropdown-menu dropdownMainMenu'>
|
||||
{Object.values(SortBy).map((item, index) => (
|
||||
<div
|
||||
key={`sortByItem-${index}`}
|
||||
className='dropdown-item dropdownMainMenuItem'
|
||||
onClick={() =>
|
||||
setFilterOptions((prev) => ({
|
||||
...prev,
|
||||
sort: item
|
||||
}))
|
||||
}
|
||||
>
|
||||
{item}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='FiltersMainElement'>
|
||||
<div className='dropdown dropdownMain'>
|
||||
<button
|
||||
className='btn dropdown-toggle btnMain btnMainDropdown'
|
||||
aria-expanded='false'
|
||||
data-bs-toggle='dropdown'
|
||||
type='button'
|
||||
>
|
||||
{filterOptions.moderated}
|
||||
</button>
|
||||
<div className='dropdown-menu dropdownMainMenu'>
|
||||
{Object.values(ModeratedFilter).map((item, index) => {
|
||||
if (item === ModeratedFilter.Unmoderated_Fully) {
|
||||
const isAdmin =
|
||||
userState.user?.npub ===
|
||||
import.meta.env.VITE_REPORTING_NPUB
|
||||
|
||||
if (!isAdmin) return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
key={`moderatedFilterItem-${index}`}
|
||||
className='dropdown-item dropdownMainMenuItem'
|
||||
onClick={() =>
|
||||
setFilterOptions((prev) => ({
|
||||
...prev,
|
||||
moderated: item
|
||||
}))
|
||||
}
|
||||
>
|
||||
{item}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='FiltersMainElement'>
|
||||
<div className='dropdown dropdownMain'>
|
||||
<button
|
||||
className='btn dropdown-toggle btnMain btnMainDropdown'
|
||||
aria-expanded='false'
|
||||
data-bs-toggle='dropdown'
|
||||
type='button'
|
||||
>
|
||||
{filterOptions.nsfw}
|
||||
</button>
|
||||
<div className='dropdown-menu dropdownMainMenu'>
|
||||
{Object.values(NSFWFilter).map((item, index) => (
|
||||
<div
|
||||
key={`nsfwFilterItem-${index}`}
|
||||
className='dropdown-item dropdownMainMenuItem'
|
||||
onClick={() =>
|
||||
setFilterOptions((prev) => ({
|
||||
...prev,
|
||||
nsfw: item
|
||||
}))
|
||||
}
|
||||
>
|
||||
{item}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='FiltersMainElement'>
|
||||
<div className='dropdown dropdownMain'>
|
||||
<button
|
||||
className='btn dropdown-toggle btnMain btnMainDropdown'
|
||||
aria-expanded='false'
|
||||
data-bs-toggle='dropdown'
|
||||
type='button'
|
||||
>
|
||||
{filterOptions.source === window.location.host
|
||||
? `Show From: ${filterOptions.source}`
|
||||
: 'Show All'}
|
||||
</button>
|
||||
<div className='dropdown-menu dropdownMainMenu'>
|
||||
<div
|
||||
className='dropdown-item dropdownMainMenuItem'
|
||||
onClick={() =>
|
||||
setFilterOptions((prev) => ({
|
||||
...prev,
|
||||
source: window.location.host
|
||||
}))
|
||||
}
|
||||
>
|
||||
Show From: {window.location.host}
|
||||
</div>
|
||||
<div
|
||||
className='dropdown-item dropdownMainMenuItem'
|
||||
onClick={() =>
|
||||
setFilterOptions((prev) => ({
|
||||
...prev,
|
||||
source: 'Show All'
|
||||
}))
|
||||
}
|
||||
>
|
||||
Show All
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
@ -3,6 +3,7 @@ import { ErrorBoundary } from 'components/ErrorBoundary'
|
||||
import { GameCard } from 'components/GameCard'
|
||||
import { LoadingSpinner } from 'components/LoadingSpinner'
|
||||
import { ModCard } from 'components/ModCard'
|
||||
import { ModFilter } from 'components/ModsFilter'
|
||||
import { Pagination } from 'components/Pagination'
|
||||
import { Profile } from 'components/ProfileSection'
|
||||
import {
|
||||
@ -11,7 +12,13 @@ import {
|
||||
T_TAG_VALUE
|
||||
} from 'constants.ts'
|
||||
import { RelayController } from 'controllers'
|
||||
import { useAppSelector, useGames, useMuteLists } from 'hooks'
|
||||
import {
|
||||
useAppSelector,
|
||||
useFilteredMods,
|
||||
useGames,
|
||||
useMuteLists,
|
||||
useNSFWList
|
||||
} from 'hooks'
|
||||
import { Filter, kinds } from 'nostr-tools'
|
||||
import { Subscription } from 'nostr-tools/abstract-relay'
|
||||
import React, {
|
||||
@ -24,48 +31,40 @@ import React, {
|
||||
} from 'react'
|
||||
import { useSearchParams } from 'react-router-dom'
|
||||
import { toast } from 'react-toastify'
|
||||
import { ModDetails, MuteLists } from 'types'
|
||||
import {
|
||||
FilterOptions,
|
||||
ModDetails,
|
||||
ModeratedFilter,
|
||||
MuteLists,
|
||||
NSFWFilter,
|
||||
SortBy
|
||||
} from 'types'
|
||||
import { extractModData, isModDataComplete, log, LogType } from 'utils'
|
||||
|
||||
enum SortByEnum {
|
||||
Latest = 'Latest',
|
||||
Oldest = 'Oldest',
|
||||
Best_Rated = 'Best Rated',
|
||||
Worst_Rated = 'Worst Rated'
|
||||
}
|
||||
|
||||
enum ModeratedFilterEnum {
|
||||
Moderated = 'Moderated',
|
||||
Unmoderated = 'Unmoderated',
|
||||
Unmoderated_Fully = 'Unmoderated Fully'
|
||||
}
|
||||
|
||||
enum SearchingFilterEnum {
|
||||
enum SearchKindEnum {
|
||||
Mods = 'Mods',
|
||||
Games = 'Games',
|
||||
Users = 'Users'
|
||||
}
|
||||
|
||||
interface FilterOptions {
|
||||
sort: SortByEnum
|
||||
moderated: ModeratedFilterEnum
|
||||
searching: SearchingFilterEnum
|
||||
source: string
|
||||
}
|
||||
|
||||
export const SearchPage = () => {
|
||||
const [searchParams] = useSearchParams()
|
||||
|
||||
const muteLists = useMuteLists()
|
||||
const nsfwList = useNSFWList()
|
||||
const searchTermRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
const [searchKind, setSearchKind] = useState(
|
||||
(searchParams.get('searching') as SearchKindEnum) || SearchKindEnum.Mods
|
||||
)
|
||||
|
||||
const [filterOptions, setFilterOptions] = useState<FilterOptions>({
|
||||
sort: SortByEnum.Latest,
|
||||
moderated: ModeratedFilterEnum.Moderated,
|
||||
sort: SortBy.Latest,
|
||||
nsfw: NSFWFilter.Hide_NSFW,
|
||||
source: window.location.host,
|
||||
searching:
|
||||
(searchParams.get('searching') as SearchingFilterEnum) ||
|
||||
SearchingFilterEnum.Mods
|
||||
moderated: ModeratedFilter.Moderated
|
||||
})
|
||||
|
||||
const [searchTerm, setSearchTerm] = useState(
|
||||
searchParams.get('searchTerm') || ''
|
||||
)
|
||||
@ -129,22 +128,25 @@ export const SearchPage = () => {
|
||||
<Filters
|
||||
filterOptions={filterOptions}
|
||||
setFilterOptions={setFilterOptions}
|
||||
searchKind={searchKind}
|
||||
setSearchKind={setSearchKind}
|
||||
/>
|
||||
{filterOptions.searching === SearchingFilterEnum.Mods && (
|
||||
{searchKind === SearchKindEnum.Mods && (
|
||||
<ModsResult
|
||||
searchTerm={searchTerm}
|
||||
filterOptions={filterOptions}
|
||||
muteLists={muteLists}
|
||||
nsfwList={nsfwList}
|
||||
/>
|
||||
)}
|
||||
{filterOptions.searching === SearchingFilterEnum.Users && (
|
||||
{searchKind === SearchKindEnum.Users && (
|
||||
<UsersResult
|
||||
searchTerm={searchTerm}
|
||||
muteLists={muteLists}
|
||||
moderationFilter={filterOptions.moderated}
|
||||
/>
|
||||
)}
|
||||
{filterOptions.searching === SearchingFilterEnum.Games && (
|
||||
{searchKind === SearchKindEnum.Games && (
|
||||
<GamesResult searchTerm={searchTerm} />
|
||||
)}
|
||||
</div>
|
||||
@ -156,49 +158,30 @@ export const SearchPage = () => {
|
||||
type FiltersProps = {
|
||||
filterOptions: FilterOptions
|
||||
setFilterOptions: Dispatch<SetStateAction<FilterOptions>>
|
||||
searchKind: SearchKindEnum
|
||||
setSearchKind: Dispatch<SetStateAction<SearchKindEnum>>
|
||||
}
|
||||
|
||||
const Filters = React.memo(
|
||||
({ filterOptions, setFilterOptions }: FiltersProps) => {
|
||||
({
|
||||
filterOptions,
|
||||
setFilterOptions,
|
||||
searchKind,
|
||||
setSearchKind
|
||||
}: FiltersProps) => {
|
||||
const userState = useAppSelector((state) => state.user)
|
||||
|
||||
return (
|
||||
<div className='IBMSecMain'>
|
||||
<div className='FiltersMain'>
|
||||
{filterOptions.searching === SearchingFilterEnum.Mods && (
|
||||
<div className='FiltersMainElement'>
|
||||
<div className='dropdown dropdownMain'>
|
||||
<button
|
||||
className='btn dropdown-toggle btnMain btnMainDropdown'
|
||||
aria-expanded='false'
|
||||
data-bs-toggle='dropdown'
|
||||
type='button'
|
||||
>
|
||||
{filterOptions.sort}
|
||||
</button>
|
||||
|
||||
<div className='dropdown-menu dropdownMainMenu'>
|
||||
{Object.values(SortByEnum).map((item, index) => (
|
||||
<div
|
||||
key={`sortByItem-${index}`}
|
||||
className='dropdown-item dropdownMainMenuItem'
|
||||
onClick={() =>
|
||||
setFilterOptions((prev) => ({
|
||||
...prev,
|
||||
sort: item
|
||||
}))
|
||||
}
|
||||
>
|
||||
{item}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{searchKind === SearchKindEnum.Mods && (
|
||||
<ModFilter
|
||||
filterOptions={filterOptions}
|
||||
setFilterOptions={setFilterOptions}
|
||||
/>
|
||||
)}
|
||||
|
||||
{(filterOptions.searching === SearchingFilterEnum.Mods ||
|
||||
filterOptions.searching === SearchingFilterEnum.Users) && (
|
||||
{searchKind === SearchKindEnum.Users && (
|
||||
<div className='FiltersMainElement'>
|
||||
<div className='dropdown dropdownMain'>
|
||||
<button
|
||||
@ -210,8 +193,8 @@ const Filters = React.memo(
|
||||
{filterOptions.moderated}
|
||||
</button>
|
||||
<div className='dropdown-menu dropdownMainMenu'>
|
||||
{Object.values(ModeratedFilterEnum).map((item, index) => {
|
||||
if (item === ModeratedFilterEnum.Unmoderated_Fully) {
|
||||
{Object.values(ModeratedFilter).map((item, index) => {
|
||||
if (item === ModeratedFilter.Unmoderated_Fully) {
|
||||
const isAdmin =
|
||||
userState.user?.npub ===
|
||||
import.meta.env.VITE_REPORTING_NPUB
|
||||
@ -239,7 +222,6 @@ const Filters = React.memo(
|
||||
</div>
|
||||
)}
|
||||
|
||||
{filterOptions.searching === SearchingFilterEnum.Mods && (
|
||||
<div className='FiltersMainElement'>
|
||||
<div className='dropdown dropdownMain'>
|
||||
<button
|
||||
@ -248,59 +230,14 @@ const Filters = React.memo(
|
||||
data-bs-toggle='dropdown'
|
||||
type='button'
|
||||
>
|
||||
{filterOptions.source === window.location.host
|
||||
? `Show From: ${filterOptions.source}`
|
||||
: 'Show All'}
|
||||
Searching: {searchKind}
|
||||
</button>
|
||||
<div className='dropdown-menu dropdownMainMenu'>
|
||||
<div
|
||||
className='dropdown-item dropdownMainMenuItem'
|
||||
onClick={() =>
|
||||
setFilterOptions((prev) => ({
|
||||
...prev,
|
||||
source: window.location.host
|
||||
}))
|
||||
}
|
||||
>
|
||||
Show From: {window.location.host}
|
||||
</div>
|
||||
<div
|
||||
className='dropdown-item dropdownMainMenuItem'
|
||||
onClick={() =>
|
||||
setFilterOptions((prev) => ({
|
||||
...prev,
|
||||
source: 'Show All'
|
||||
}))
|
||||
}
|
||||
>
|
||||
Show All
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className='FiltersMainElement'>
|
||||
<div className='dropdown dropdownMain'>
|
||||
<button
|
||||
className='btn dropdown-toggle btnMain btnMainDropdown'
|
||||
aria-expanded='false'
|
||||
data-bs-toggle='dropdown'
|
||||
type='button'
|
||||
>
|
||||
Searching: {filterOptions.searching}
|
||||
</button>
|
||||
<div className='dropdown-menu dropdownMainMenu'>
|
||||
{Object.values(SearchingFilterEnum).map((item, index) => (
|
||||
{Object.values(SearchKindEnum).map((item, index) => (
|
||||
<div
|
||||
key={`searchingFilterItem-${index}`}
|
||||
className='dropdown-item dropdownMainMenuItem'
|
||||
onClick={() =>
|
||||
setFilterOptions((prev) => ({
|
||||
...prev,
|
||||
searching: item
|
||||
}))
|
||||
}
|
||||
onClick={() => setSearchKind(item)}
|
||||
>
|
||||
{item}
|
||||
</div>
|
||||
@ -321,12 +258,14 @@ type ModsResultProps = {
|
||||
admin: MuteLists
|
||||
user: MuteLists
|
||||
}
|
||||
nsfwList: string[]
|
||||
}
|
||||
|
||||
const ModsResult = ({
|
||||
filterOptions,
|
||||
searchTerm,
|
||||
muteLists
|
||||
muteLists,
|
||||
nsfwList
|
||||
}: ModsResultProps) => {
|
||||
const hasEffectRun = useRef(false)
|
||||
const [isSubscribing, setIsSubscribing] = useState(false)
|
||||
@ -400,49 +339,13 @@ const ModsResult = ({
|
||||
return mods.filter(filterFn)
|
||||
}, [mods, searchTerm])
|
||||
|
||||
const filteredModList = useMemo(() => {
|
||||
let filtered: ModDetails[] = [...filteredMods]
|
||||
|
||||
if (filterOptions.source === window.location.host) {
|
||||
filtered = filtered.filter((mod) => mod.rTag === window.location.host)
|
||||
}
|
||||
|
||||
const isAdmin = userState.user?.npub === import.meta.env.VITE_REPORTING_NPUB
|
||||
const isUnmoderatedFully =
|
||||
filterOptions.moderated === ModeratedFilterEnum.Unmoderated_Fully
|
||||
|
||||
// Only apply filtering if the user is not an admin or the admin has not selected "Unmoderated Fully"
|
||||
if (!(isAdmin && isUnmoderatedFully)) {
|
||||
filtered = filtered.filter(
|
||||
(mod) =>
|
||||
!muteLists.admin.authors.includes(mod.author) &&
|
||||
!muteLists.admin.replaceableEvents.includes(mod.aTag)
|
||||
)
|
||||
}
|
||||
|
||||
if (filterOptions.moderated === ModeratedFilterEnum.Moderated) {
|
||||
filtered = filtered.filter(
|
||||
(mod) =>
|
||||
!muteLists.user.authors.includes(mod.author) &&
|
||||
!muteLists.user.replaceableEvents.includes(mod.aTag)
|
||||
)
|
||||
}
|
||||
|
||||
if (filterOptions.sort === SortByEnum.Latest) {
|
||||
filtered.sort((a, b) => b.published_at - a.published_at)
|
||||
} else if (filterOptions.sort === SortByEnum.Oldest) {
|
||||
filtered.sort((a, b) => a.published_at - b.published_at)
|
||||
}
|
||||
|
||||
return filtered
|
||||
}, [
|
||||
const filteredModList = useFilteredMods(
|
||||
filteredMods,
|
||||
userState.user?.npub,
|
||||
filterOptions.sort,
|
||||
filterOptions.moderated,
|
||||
filterOptions.source,
|
||||
userState,
|
||||
filterOptions,
|
||||
nsfwList,
|
||||
muteLists
|
||||
])
|
||||
)
|
||||
|
||||
const handleNext = () => {
|
||||
setPage((prev) => prev + 1)
|
||||
@ -478,7 +381,7 @@ const ModsResult = ({
|
||||
|
||||
type UsersResultProps = {
|
||||
searchTerm: string
|
||||
moderationFilter: ModeratedFilterEnum
|
||||
moderationFilter: ModeratedFilter
|
||||
muteLists: {
|
||||
admin: MuteLists
|
||||
user: MuteLists
|
||||
@ -528,7 +431,7 @@ const UsersResult = ({
|
||||
let filtered = [...profiles]
|
||||
const isAdmin = userState.user?.npub === import.meta.env.VITE_REPORTING_NPUB
|
||||
const isUnmoderatedFully =
|
||||
moderationFilter === ModeratedFilterEnum.Unmoderated_Fully
|
||||
moderationFilter === ModeratedFilter.Unmoderated_Fully
|
||||
|
||||
// Only apply filtering if the user is not an admin or the admin has not selected "Unmoderated Fully"
|
||||
if (!(isAdmin && isUnmoderatedFully)) {
|
||||
@ -537,7 +440,7 @@ const UsersResult = ({
|
||||
)
|
||||
}
|
||||
|
||||
if (moderationFilter === ModeratedFilterEnum.Moderated) {
|
||||
if (moderationFilter === ModeratedFilter.Moderated) {
|
||||
filtered = filtered.filter(
|
||||
(profile) => !muteLists.user.authors.includes(profile.pubkey as string)
|
||||
)
|
||||
|
@ -1,4 +1,5 @@
|
||||
export * from './mod'
|
||||
export * from './modsFilter'
|
||||
export * from './nostr'
|
||||
export * from './user'
|
||||
export * from './zap'
|
||||
|
25
src/types/modsFilter.ts
Normal file
25
src/types/modsFilter.ts
Normal file
@ -0,0 +1,25 @@
|
||||
export enum SortBy {
|
||||
Latest = 'Latest',
|
||||
Oldest = 'Oldest',
|
||||
Best_Rated = 'Best Rated',
|
||||
Worst_Rated = 'Worst Rated'
|
||||
}
|
||||
|
||||
export enum NSFWFilter {
|
||||
Hide_NSFW = 'Hide NSFW',
|
||||
Show_NSFW = 'Show NSFW',
|
||||
Only_NSFW = 'Only NSFW'
|
||||
}
|
||||
|
||||
export enum ModeratedFilter {
|
||||
Moderated = 'Moderated',
|
||||
Unmoderated = 'Unmoderated',
|
||||
Unmoderated_Fully = 'Unmoderated Fully'
|
||||
}
|
||||
|
||||
export interface FilterOptions {
|
||||
sort: SortBy
|
||||
nsfw: NSFWFilter
|
||||
source: string
|
||||
moderated: ModeratedFilter
|
||||
}
|
Loading…
Reference in New Issue
Block a user