user profile btn in social nav now only is active for current logged in user, added NSFW tag for admin tagged ones, mod search under a specific game, search term and some filters added to url, filter state is saved locally in cache, user search now works #108

Merged
freakoverse merged 21 commits from staging into master 2024-10-30 16:00:24 +00:00
23 changed files with 50557 additions and 50385 deletions

View File

@ -1,7 +1,7 @@
{ {
"name": "degmods.com", "name": "degmods.com",
"private": true, "private": true,
"version": "0.0.0", "version": "0.0.0-alpha",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",

View File

@ -7909,7 +7909,7 @@ Lamborghini R6 125,,
Farming Simulator 2013 Ursus,, Farming Simulator 2013 Ursus,,
Cubemen Soundtrack,, Cubemen Soundtrack,,
Nancy Drew: The Deadly Device,, Nancy Drew: The Deadly Device,,
DmC Devil May Cry,, DmC Devil May Cry,,https://image.nostr.build/917da143fafe8a003865ec4dbbb872dcb04020fad2ca8d3c6cee00f2ac141bde.jpg
Cargo Commander,, Cargo Commander,,
Fairy Bloom Freesia Demo,, Fairy Bloom Freesia Demo,,
Football Manager 2013 Russian Demo,, Football Manager 2013 Russian Demo,,
@ -9057,7 +9057,7 @@ Hacker Evolution Duality Hardcore Package 1,,
MC6T - Cakewalk Expansion Pack - Modern Strings,, MC6T - Cakewalk Expansion Pack - Modern Strings,,
MC6T - Cakewalk Expansion Pack - Guitars,, MC6T - Cakewalk Expansion Pack - Guitars,,
Sorcerer King,, Sorcerer King,,
Assassin's Creed IV Black Flag,, Assassin's Creed IV Black Flag,,https://image.nostr.build/6e468d37073f922b9442e03a9428e10425032dbae19a920316ebc32520c10713.jpg
Joe Danger 2: The Movie,, Joe Danger 2: The Movie,,
Joe Danger 2: Undead Movie Pack,, Joe Danger 2: Undead Movie Pack,,
Vector Thrust,, Vector Thrust,,
@ -39626,7 +39626,7 @@ Red Bull 360: Get the ultimate 360 video experience of drifting,,
8Doors: Arum's Afterlife Adventure,, 8Doors: Arum's Afterlife Adventure,,
Thomaz,, Thomaz,,
Jack & the Cat,, Jack & the Cat,,
Atomic Heart,, Atomic Heart,,https://image.nostr.build/19cf6181c271a6e2d56dd275600a29b797569f3e054a162bae63ca46c255e772.jpg
Omen Exitio: Plague,, Omen Exitio: Plague,,
Pixelum,, Pixelum,,
Wars of Seignior,, Wars of Seignior,,

Can't render this file because it is too large.

View File

@ -1,16 +1,20 @@
import { useAppSelector } from 'hooks' import { useAppSelector, useLocalStorage } from 'hooks'
import React from 'react' import React from 'react'
import { Dispatch, SetStateAction } from 'react'
import { FilterOptions, ModeratedFilter, NSFWFilter, SortBy } from 'types' import { FilterOptions, ModeratedFilter, NSFWFilter, SortBy } from 'types'
import { DEFAULT_FILTER_OPTIONS } from 'utils'
type Props = { type Props = {
filterOptions: FilterOptions author?: string | undefined
setFilterOptions: Dispatch<SetStateAction<FilterOptions>> filterKey?: string | undefined
} }
export const ModFilter = React.memo( export const ModFilter = React.memo(
({ filterOptions, setFilterOptions }: Props) => { ({ author, filterKey = 'filter' }: Props) => {
const userState = useAppSelector((state) => state.user) const userState = useAppSelector((state) => state.user)
const [filterOptions, setFilterOptions] = useLocalStorage<FilterOptions>(
filterKey,
DEFAULT_FILTER_OPTIONS
)
return ( return (
<div className='IBMSecMain'> <div className='IBMSecMain'>
@ -62,9 +66,9 @@ export const ModFilter = React.memo(
import.meta.env.VITE_REPORTING_NPUB import.meta.env.VITE_REPORTING_NPUB
const isOwnProfile = const isOwnProfile =
filterOptions.author && author &&
userState.auth && userState.auth &&
userState.user?.pubkey === filterOptions.author userState.user?.pubkey === author
if (!(isAdmin || isOwnProfile)) return null if (!(isAdmin || isOwnProfile)) return null
} }

View File

@ -14,7 +14,7 @@ import { appRoutes, getProfilePageRoute } from '../routes'
import '../styles/author.css' import '../styles/author.css'
import '../styles/innerPage.css' import '../styles/innerPage.css'
import '../styles/socialPosts.css' import '../styles/socialPosts.css'
import { UserProfile, UserRelaysType } from '../types' import { UserRelaysType } from '../types'
import { import {
copyTextToClipboard, copyTextToClipboard,
hexToNpub, hexToNpub,
@ -27,37 +27,18 @@ import { LoadingSpinner } from './LoadingSpinner'
import { ZapPopUp } from './Zap' import { ZapPopUp } from './Zap'
import placeholder from '../assets/img/DEGMods Placeholder Img.png' import placeholder from '../assets/img/DEGMods Placeholder Img.png'
import { NDKEvent } from '@nostr-dev-kit/ndk' import { NDKEvent } from '@nostr-dev-kit/ndk'
import { useProfile } from 'hooks/useProfile'
type Props = { type Props = {
pubkey: string pubkey: string
} }
export const ProfileSection = ({ pubkey }: Props) => { export const ProfileSection = ({ pubkey }: Props) => {
const { findMetadata } = useNDKContext()
const [profile, setProfile] = useState<UserProfile>()
useDidMount(() => {
findMetadata(pubkey).then((res) => {
setProfile(res)
})
})
const displayName =
profile?.displayName || profile?.name || '[name not set up]'
const about = profile?.bio || profile?.about || '[bio not set up]'
return ( return (
<div className='IBMSMSplitMainSmallSide'> <div className='IBMSMSplitMainSmallSide'>
<div className='IBMSMSplitMainSmallSideSecWrapper'> <div className='IBMSMSplitMainSmallSideSecWrapper'>
<div className='IBMSMSplitMainSmallSideSec'> <div className='IBMSMSplitMainSmallSideSec'>
<Profile <Profile pubkey={pubkey} />
pubkey={pubkey}
displayName={displayName}
about={about}
image={profile?.image}
nip05={profile?.nip05}
lud16={profile?.lud16}
/>
</div> </div>
<div className='IBMSMSplitMainSmallSideSec'> <div className='IBMSMSplitMainSmallSideSec'>
<div className='IBMSMSMSSS_ShortPosts'> <div className='IBMSMSMSSS_ShortPosts'>
@ -109,21 +90,18 @@ export const ProfileSection = ({ pubkey }: Props) => {
type ProfileProps = { type ProfileProps = {
pubkey: string pubkey: string
displayName: string
about: string
image?: string
nip05?: string
lud16?: string
} }
export const Profile = ({ export const Profile = ({ pubkey }: ProfileProps) => {
pubkey, const profile = useProfile(pubkey)
displayName,
about, const displayName =
image, profile?.displayName || profile?.name || '[name not set up]'
nip05, const about = profile?.bio || profile?.about || '[bio not set up]'
lud16 const image = profile?.image || FALLBACK_PROFILE_IMAGE
}: ProfileProps) => { const nip05 = profile?.nip05
const lud16 = profile?.lud16
const npub = hexToNpub(pubkey) const npub = hexToNpub(pubkey)
const handleCopy = async () => { const handleCopy = async () => {
@ -138,14 +116,20 @@ export const Profile = ({
}) })
} }
// Try to encode
let profileRoute = appRoutes.home let profileRoute = appRoutes.home
let nprofile: string | undefined
try {
const hexPubkey = npubToHex(pubkey) const hexPubkey = npubToHex(pubkey)
if (hexPubkey) { nprofile = hexPubkey
profileRoute = getProfilePageRoute( ? nip19.nprofileEncode({
nip19.nprofileEncode({
pubkey: hexPubkey pubkey: hexPubkey
}) })
) : undefined
profileRoute = nprofile ? getProfilePageRoute(nprofile) : appRoutes.home
} catch (error) {
// Silently ignore and redirect to home
log(true, LogType.Error, 'Failed to encode profile.', error)
} }
return ( return (
@ -162,9 +146,7 @@ export const Profile = ({
<div <div
className='IBMSMSMSSS_Author_Top_PP' className='IBMSMSMSSS_Author_Top_PP'
style={{ style={{
background: `url('${ background: `url('${image}') center / cover no-repeat`
image || FALLBACK_PROFILE_IMAGE
}') center / cover no-repeat`
}} }}
></div> ></div>
</div> </div>
@ -172,7 +154,8 @@ export const Profile = ({
<div className='IBMSMSMSSS_Author_Top_Left_InsideDetails'> <div className='IBMSMSMSSS_Author_Top_Left_InsideDetails'>
<div className='IBMSMSMSSS_Author_TopWrapper'> <div className='IBMSMSMSSS_Author_TopWrapper'>
<p className='IBMSMSMSSS_Author_Top_Name'>{displayName}</p> <p className='IBMSMSMSSS_Author_Top_Name'>{displayName}</p>
{nip05 && ( {/* Nip05 can sometimes be an empty object '{}' which causes the error */}
{typeof nip05 === 'string' && (
<p className='IBMSMSMSSS_Author_Top_Handle'>{nip05}</p> <p className='IBMSMSMSSS_Author_Top_Handle'>{nip05}</p>
)} )}
</div> </div>
@ -205,8 +188,12 @@ export const Profile = ({
<path d='M384 96L384 0h-112c-26.51 0-48 21.49-48 48v288c0 26.51 21.49 48 48 48H464c26.51 0 48-21.49 48-48V128h-95.1C398.4 128 384 113.6 384 96zM416 0v96h96L416 0zM192 352V128h-144c-26.51 0-48 21.49-48 48v288c0 26.51 21.49 48 48 48h192c26.51 0 48-21.49 48-48L288 416h-32C220.7 416 192 387.3 192 352z'></path> <path d='M384 96L384 0h-112c-26.51 0-48 21.49-48 48v288c0 26.51 21.49 48 48 48H464c26.51 0 48-21.49 48-48V128h-95.1C398.4 128 384 113.6 384 96zM416 0v96h96L416 0zM192 352V128h-144c-26.51 0-48 21.49-48 48v288c0 26.51 21.49 48 48 48h192c26.51 0 48-21.49 48-48L288 416h-32C220.7 416 192 387.3 192 352z'></path>
</svg> </svg>
</div> </div>
<ProfileQRButtonWithPopUp pubkey={pubkey} /> {typeof nprofile !== 'undefined' && (
{lud16 && <ZapButtonWithPopUp pubkey={pubkey} />} <ProfileQRButtonWithPopUp nprofile={nprofile} />
)}
{typeof lud16 !== 'undefined' && (
<ZapButtonWithPopUp pubkey={pubkey} />
)}
</div> </div>
</div> </div>
</div> </div>
@ -251,20 +238,16 @@ const posts: Post[] = [
] ]
type QRButtonWithPopUpProps = { type QRButtonWithPopUpProps = {
pubkey: string nprofile: string
} }
export const ProfileQRButtonWithPopUp = ({ export const ProfileQRButtonWithPopUp = ({
pubkey nprofile
}: QRButtonWithPopUpProps) => { }: QRButtonWithPopUpProps) => {
const [isOpen, setIsOpen] = useState(false) const [isOpen, setIsOpen] = useState(false)
useBodyScrollDisable(isOpen) useBodyScrollDisable(isOpen)
const nprofile = nip19.nprofileEncode({
pubkey
})
const onQrCodeClicked = async () => { const onQrCodeClicked = async () => {
const href = `https://njump.me/${nprofile}` const href = `https://njump.me/${nprofile}`
const a = document.createElement('a') const a = document.createElement('a')

View File

@ -0,0 +1,39 @@
import { forwardRef } from 'react'
interface SearchInputProps {
handleKeyDown: (event: React.KeyboardEvent<HTMLInputElement>) => void
handleSearch: () => void
}
export const SearchInput = forwardRef<HTMLInputElement, SearchInputProps>(
({ handleKeyDown, handleSearch }, ref) => (
<div className='SearchMain'>
<div className='SearchMainInside'>
<div className='SearchMainInsideWrapper'>
<input
type='text'
className='SMIWInput'
ref={ref}
onKeyDown={handleKeyDown}
placeholder='Enter search term'
/>
<button
className='btn btnMain SMIWButton'
type='button'
onClick={handleSearch}
>
<svg
xmlns='http://www.w3.org/2000/svg'
viewBox='0 0 512 512'
width='1em'
height='1em'
fill='currentColor'
>
<path d='M500.3 443.7l-119.7-119.7c27.22-40.41 40.65-90.9 33.46-144.7C401.8 87.79 326.8 13.32 235.2 1.723C99.01-15.51-15.51 99.01 1.724 235.2c11.6 91.64 86.08 166.7 177.6 178.9c53.8 7.189 104.3-6.236 144.7-33.46l119.7 119.7c15.62 15.62 40.95 15.62 56.57 0C515.9 484.7 515.9 459.3 500.3 443.7zM79.1 208c0-70.58 57.42-128 128-128s128 57.42 128 128c0 70.58-57.42 128-128 128S79.1 278.6 79.1 208z'></path>
</svg>
</button>
</div>
</div>
</div>
)
)

View File

@ -7,3 +7,4 @@ export * from './useNSFWList'
export * from './useReactions' export * from './useReactions'
export * from './useNDKContext' export * from './useNDKContext'
export * from './useScrollDisable' export * from './useScrollDisable'
export * from './useLocalStorage'

View File

@ -18,10 +18,20 @@ export const useFilteredMods = (
muteLists: { muteLists: {
admin: MuteLists admin: MuteLists
user: MuteLists user: MuteLists
} },
author?: string | undefined
) => { ) => {
return useMemo(() => { return useMemo(() => {
const nsfwFilter = (mods: ModDetails[]) => { const nsfwFilter = (mods: ModDetails[]) => {
// Add nsfw tag to mods included in nsfwList
if (filterOptions.nsfw !== NSFWFilter.Hide_NSFW) {
mods = mods.map((mod) => {
return !mod.nsfw && nsfwList.includes(mod.aTag)
? { ...mod, nsfw: true }
: mod
})
}
// Determine the filtering logic based on the NSFW filter option // Determine the filtering logic based on the NSFW filter option
switch (filterOptions.nsfw) { switch (filterOptions.nsfw) {
case NSFWFilter.Hide_NSFW: case NSFWFilter.Hide_NSFW:
@ -41,7 +51,7 @@ export const useFilteredMods = (
const isAdmin = userState.user?.npub === import.meta.env.VITE_REPORTING_NPUB const isAdmin = userState.user?.npub === import.meta.env.VITE_REPORTING_NPUB
const isOwner = const isOwner =
userState.user?.npub && userState.user?.npub &&
npubToHex(userState.user.npub as string) === filterOptions.author npubToHex(userState.user.npub as string) === author
const isUnmoderatedFully = const isUnmoderatedFully =
filterOptions.moderated === ModeratedFilter.Unmoderated_Fully filterOptions.moderated === ModeratedFilter.Unmoderated_Fully
@ -75,7 +85,7 @@ export const useFilteredMods = (
filterOptions.sort, filterOptions.sort,
filterOptions.moderated, filterOptions.moderated,
filterOptions.nsfw, filterOptions.nsfw,
filterOptions.author, author,
mods, mods,
muteLists, muteLists,
nsfwList nsfwList

View File

@ -0,0 +1,50 @@
import React from 'react'
import {
getLocalStorageItem,
removeLocalStorageItem,
setLocalStorageItem
} from 'utils'
const useLocalStorageSubscribe = (callback: () => void) => {
window.addEventListener('storage', callback)
return () => window.removeEventListener('storage', callback)
}
export function useLocalStorage<T>(
key: string,
initialValue: T
): [T, React.Dispatch<React.SetStateAction<T>>] {
const getSnapshot = () => getLocalStorageItem(key, initialValue)
const data = React.useSyncExternalStore(useLocalStorageSubscribe, getSnapshot)
const setState: React.Dispatch<React.SetStateAction<T>> = React.useCallback(
(v: React.SetStateAction<T>) => {
try {
const nextState =
typeof v === 'function'
? (v as (prevState: T) => T)(JSON.parse(data))
: v
if (nextState === undefined || nextState === null) {
removeLocalStorageItem(key)
} else {
setLocalStorageItem(key, JSON.stringify(nextState))
}
} catch (e) {
console.warn(e)
}
},
[key, data]
)
React.useEffect(() => {
// Set local storage only when it's empty
const data = window.localStorage.getItem(key)
if (data === null) {
setLocalStorageItem(key, JSON.stringify(initialValue))
}
}, [key, initialValue])
return [JSON.parse(data) as T, setState]
}

18
src/hooks/useProfile.tsx Normal file
View File

@ -0,0 +1,18 @@
import { useNDKContext } from 'hooks'
import { useState, useEffect } from 'react'
import { UserProfile } from 'types'
export const useProfile = (pubkey?: string) => {
const { findMetadata } = useNDKContext()
const [profile, setProfile] = useState<UserProfile>()
useEffect(() => {
if (pubkey) {
findMetadata(pubkey).then((res) => {
setProfile(res)
})
}
}, [findMetadata, pubkey])
return profile
}

View File

@ -1,13 +1,28 @@
import { useAppSelector } from 'hooks' import { useAppSelector } from 'hooks'
import { nip19 } from 'nostr-tools'
import { useState } from 'react' import { useState } from 'react'
import { NavLink, NavLinkProps } from 'react-router-dom' import { NavLink, NavLinkProps } from 'react-router-dom'
import { appRoutes, getProfilePageRoute } from 'routes' import { appRoutes, getProfilePageRoute } from 'routes'
import 'styles/socialNav.css' import 'styles/socialNav.css'
import { npubToHex } from 'utils'
export const SocialNav = () => { export const SocialNav = () => {
const [isCollapsed, setIsCollapsed] = useState<boolean>(false) const [isCollapsed, setIsCollapsed] = useState<boolean>(false)
const userState = useAppSelector((state) => state.user) const userState = useAppSelector((state) => state.user)
let profileRoute = ''
if (userState.auth && userState.user) {
// Redirect to user's profile is no profile is linked
const userHexKey = npubToHex(userState.user.npub as string)
if (userHexKey) {
profileRoute = getProfilePageRoute(
nip19.nprofileEncode({
pubkey: userHexKey
})
)
}
}
const toggleNav = () => { const toggleNav = () => {
setIsCollapsed(!isCollapsed) setIsCollapsed(!isCollapsed)
} }
@ -42,7 +57,7 @@ export const SocialNav = () => {
/> />
{!!userState.auth && ( {!!userState.auth && (
<NavButton <NavButton
to={getProfilePageRoute('')} to={profileRoute}
svgPath='M256 288c79.53 0 144-64.47 144-144s-64.47-144-144-144c-79.52 0-144 64.47-144 144S176.5 288 256 288zM351.1 320H160c-88.36 0-160 71.63-160 160c0 17.67 14.33 32 31.1 32H480c17.67 0 31.1-14.33 31.1-32C512 391.6 440.4 320 351.1 320z' svgPath='M256 288c79.53 0 144-64.47 144-144s-64.47-144-144-144c-79.52 0-144 64.47-144 144S176.5 288 256 288zM351.1 320H160c-88.36 0-160 71.63-160 160c0 17.67 14.33 32 31.1 32H480c17.67 0 31.1-14.33 31.1-32C512 391.6 440.4 320 351.1 320z'
/> />
)} )}

View File

@ -6,24 +6,25 @@ import {
import { ModCard } from 'components/ModCard' import { ModCard } from 'components/ModCard'
import { ModFilter } from 'components/ModsFilter' import { ModFilter } from 'components/ModsFilter'
import { PaginationWithPageNumbers } from 'components/Pagination' import { PaginationWithPageNumbers } from 'components/Pagination'
import { SearchInput } from 'components/SearchInput'
import { MAX_MODS_PER_PAGE, T_TAG_VALUE } from 'constants.ts' import { MAX_MODS_PER_PAGE, T_TAG_VALUE } from 'constants.ts'
import { import {
useAppSelector, useAppSelector,
useFilteredMods, useFilteredMods,
useLocalStorage,
useMuteLists, useMuteLists,
useNDKContext, useNDKContext,
useNSFWList useNSFWList
} from 'hooks' } from 'hooks'
import { useEffect, useRef, useState } from 'react' import { useEffect, useMemo, useRef, useState } from 'react'
import { useParams } from 'react-router-dom' import { useParams, useSearchParams } from 'react-router-dom'
import { FilterOptions, ModDetails } from 'types'
import { import {
FilterOptions, DEFAULT_FILTER_OPTIONS,
ModDetails, extractModData,
ModeratedFilter, isModDataComplete,
NSFWFilter, scrollIntoView
SortBy } from 'utils'
} from 'types'
import { extractModData, isModDataComplete, scrollIntoView } from 'utils'
export const GamePage = () => { export const GamePage = () => {
const scrollTargetRef = useRef<HTMLDivElement>(null) const scrollTargetRef = useRef<HTMLDivElement>(null)
@ -33,20 +34,70 @@ export const GamePage = () => {
const muteLists = useMuteLists() const muteLists = useMuteLists()
const nsfwList = useNSFWList() const nsfwList = useNSFWList()
const [filterOptions, setFilterOptions] = useState<FilterOptions>({ const [filterOptions] = useLocalStorage<FilterOptions>(
sort: SortBy.Latest, 'filter',
nsfw: NSFWFilter.Hide_NSFW, DEFAULT_FILTER_OPTIONS
source: window.location.host, )
moderated: ModeratedFilter.Moderated
})
const [mods, setMods] = useState<ModDetails[]>([]) const [mods, setMods] = useState<ModDetails[]>([])
const [currentPage, setCurrentPage] = useState(1) const [currentPage, setCurrentPage] = useState(1)
const userState = useAppSelector((state) => state.user) const userState = useAppSelector((state) => state.user)
const filteredMods = useFilteredMods( // Search
mods, const searchTermRef = useRef<HTMLInputElement>(null)
const [searchParams, setSearchParams] = useSearchParams()
const [searchTerm, setSearchTerm] = useState(searchParams.get('q') || '')
const handleSearch = () => {
const value = searchTermRef.current?.value || '' // Access the input value from the ref
setSearchTerm(value)
if (value) {
searchParams.set('q', value)
} else {
searchParams.delete('q')
}
setSearchParams(searchParams, {
replace: true
})
}
// Handle "Enter" key press inside the input
const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
if (event.key === 'Enter') {
handleSearch()
}
}
const filteredMods = useMemo(() => {
const filterSourceFn = (mod: ModDetails) => {
if (filterOptions.source === window.location.host) {
return mod.rTag === filterOptions.source
}
return true
}
// If search term is missing, only filter by sources
if (searchTerm === '') return mods.filter(filterSourceFn)
const lowerCaseSearchTerm = searchTerm.toLowerCase()
const filterFn = (mod: ModDetails) =>
mod.title.toLowerCase().includes(lowerCaseSearchTerm) ||
mod.game.toLowerCase().includes(lowerCaseSearchTerm) ||
mod.summary.toLowerCase().includes(lowerCaseSearchTerm) ||
mod.body.toLowerCase().includes(lowerCaseSearchTerm) ||
mod.tags.findIndex((tag) =>
tag.toLowerCase().includes(lowerCaseSearchTerm)
) > -1
return mods.filter(filterFn).filter(filterSourceFn)
}, [filterOptions.source, mods, searchTerm])
const filteredModList = useFilteredMods(
filteredMods,
userState, userState,
filterOptions, filterOptions,
nsfwList, nsfwList,
@ -54,11 +105,11 @@ export const GamePage = () => {
) )
// Pagination logic // Pagination logic
const totalGames = filteredMods.length const totalGames = filteredModList.length
const totalPages = Math.ceil(totalGames / MAX_MODS_PER_PAGE) const totalPages = Math.ceil(totalGames / MAX_MODS_PER_PAGE)
const startIndex = (currentPage - 1) * MAX_MODS_PER_PAGE const startIndex = (currentPage - 1) * MAX_MODS_PER_PAGE
const endIndex = startIndex + MAX_MODS_PER_PAGE const endIndex = startIndex + MAX_MODS_PER_PAGE
const currentMods = filteredMods.slice(startIndex, endIndex) const currentMods = filteredModList.slice(startIndex, endIndex)
const handlePageChange = (page: number) => { const handlePageChange = (page: number) => {
if (page >= 1 && page <= totalPages) { if (page >= 1 && page <= totalPages) {
@ -116,14 +167,24 @@ export const GamePage = () => {
<span className='IBMSMTitleMainHeadingSpan'> <span className='IBMSMTitleMainHeadingSpan'>
{gameName} {gameName}
</span> </span>
{searchTerm !== '' && (
<>
&nbsp;&mdash;&nbsp;
<span className='IBMSMTitleMainHeadingSpan'>
{searchTerm}
</span>
</>
)}
</h2> </h2>
</div> </div>
</div> <SearchInput
</div> handleKeyDown={handleKeyDown}
<ModFilter handleSearch={handleSearch}
filterOptions={filterOptions} ref={searchTermRef}
setFilterOptions={setFilterOptions}
/> />
</div>
</div>
<ModFilter />
<div className='IBMSecMain IBMSMListWrapper'> <div className='IBMSecMain IBMSMListWrapper'>
<div className='IBMSMList'> <div className='IBMSMList'>
{currentMods.map((mod) => ( {currentMods.map((mod) => (

View File

@ -9,6 +9,7 @@ import '../styles/styles.css'
import { createSearchParams, useNavigate } from 'react-router-dom' import { createSearchParams, useNavigate } from 'react-router-dom'
import { appRoutes } from 'routes' import { appRoutes } from 'routes'
import { scrollIntoView } from 'utils' import { scrollIntoView } from 'utils'
import { SearchInput } from 'components/SearchInput'
export const GamesPage = () => { export const GamesPage = () => {
const scrollTargetRef = useRef<HTMLDivElement>(null) const scrollTargetRef = useRef<HTMLDivElement>(null)
@ -74,8 +75,8 @@ export const GamesPage = () => {
const value = searchTermRef.current?.value || '' // Access the input value from the ref const value = searchTermRef.current?.value || '' // Access the input value from the ref
if (value !== '') { if (value !== '') {
const searchParams = createSearchParams({ const searchParams = createSearchParams({
searchTerm: value, q: value,
searching: 'Games' kind: 'Games'
}) })
navigate({ pathname: appRoutes.search, search: `?${searchParams}` }) navigate({ pathname: appRoutes.search, search: `?${searchParams}` })
} }
@ -100,34 +101,11 @@ export const GamesPage = () => {
<div className='IBMSMTitleMain'> <div className='IBMSMTitleMain'>
<h2 className='IBMSMTitleMainHeading'>Games</h2> <h2 className='IBMSMTitleMainHeading'>Games</h2>
</div> </div>
<div className='SearchMain'> <SearchInput
<div className='SearchMainInside'>
<div className='SearchMainInsideWrapper'>
<input
type='text'
className='SMIWInput'
ref={searchTermRef} ref={searchTermRef}
onKeyDown={handleKeyDown} handleKeyDown={handleKeyDown}
placeholder='Enter search term' handleSearch={handleSearch}
/> />
<button
className='btn btnMain SMIWButton'
type='button'
onClick={handleSearch}
>
<svg
xmlns='http://www.w3.org/2000/svg'
viewBox='0 0 512 512'
width='1em'
height='1em'
fill='currentColor'
>
<path d='M500.3 443.7l-119.7-119.7c27.22-40.41 40.65-90.9 33.46-144.7C401.8 87.79 326.8 13.32 235.2 1.723C99.01-15.51-15.51 99.01 1.724 235.2c11.6 91.64 86.08 166.7 177.6 178.9c53.8 7.189 104.3-6.236 144.7-33.46l119.7 119.7c15.62 15.62 40.95 15.62 56.57 0C515.9 484.7 515.9 459.3 500.3 443.7zM79.1 208c0-70.58 57.42-128 128-128s128 57.42 128 128c0 70.58-57.42 128-128 128S79.1 278.6 79.1 208z'></path>
</svg>
</button>
</div>
</div>
</div>
</div> </div>
</div> </div>
<div className='IBMSecMain IBMSMListWrapper'> <div className='IBMSecMain IBMSMListWrapper'>

View File

@ -15,7 +15,8 @@ import {
useAppSelector, useAppSelector,
useBodyScrollDisable, useBodyScrollDisable,
useDidMount, useDidMount,
useNDKContext useNDKContext,
useNSFWList
} from '../../hooks' } from '../../hooks'
import { getGamePageRoute, getModsEditPageRoute } from '../../routes' import { getGamePageRoute, getModsEditPageRoute } from '../../routes'
import '../../styles/comments.css' import '../../styles/comments.css'
@ -51,10 +52,18 @@ import placeholder from '../../assets/img/DEGMods Placeholder Img.png'
export const ModPage = () => { export const ModPage = () => {
const { naddr } = useParams() const { naddr } = useParams()
const { fetchEvent } = useNDKContext() const { fetchEvent } = useNDKContext()
const [modData, setModData] = useState<ModDetails>() const [mod, setMod] = useState<ModDetails>()
const [isFetching, setIsFetching] = useState(true) const [isFetching, setIsFetching] = useState(true)
const [commentCount, setCommentCount] = useState(0) const [commentCount, setCommentCount] = useState(0)
// Make sure to mark non-nsfw mods as NSFW if found in nsfwList
const nsfwList = useNSFWList()
const isMissingNsfwTag =
!mod?.nsfw && mod?.aTag && nsfwList && nsfwList.includes(mod.aTag)
const modData = isMissingNsfwTag
? ({ ...mod, nsfw: true } as ModDetails)
: mod
useDidMount(async () => { useDidMount(async () => {
if (naddr) { if (naddr) {
const decoded = nip19.decode<'naddr'>(naddr as `naddr1${string}`) const decoded = nip19.decode<'naddr'>(naddr as `naddr1${string}`)
@ -70,7 +79,7 @@ export const ModPage = () => {
.then((event) => { .then((event) => {
if (event) { if (event) {
const extracted = extractModData(event) const extracted = extractModData(event)
setModData(extracted) setMod(extracted)
} }
}) })
.catch((err) => { .catch((err) => {

View File

@ -8,6 +8,7 @@ import { MOD_FILTER_LIMIT } from '../constants'
import { import {
useAppSelector, useAppSelector,
useFilteredMods, useFilteredMods,
useLocalStorage,
useMuteLists, useMuteLists,
useNDKContext, useNDKContext,
useNSFWList useNSFWList
@ -17,26 +18,20 @@ import '../styles/filters.css'
import '../styles/pagination.css' import '../styles/pagination.css'
import '../styles/search.css' import '../styles/search.css'
import '../styles/styles.css' import '../styles/styles.css'
import { import { FilterOptions, ModDetails } from '../types'
FilterOptions, import { DEFAULT_FILTER_OPTIONS, scrollIntoView } from 'utils'
ModDetails, import { SearchInput } from 'components/SearchInput'
ModeratedFilter,
NSFWFilter,
SortBy
} from '../types'
import { scrollIntoView } from 'utils'
export const ModsPage = () => { export const ModsPage = () => {
const scrollTargetRef = useRef<HTMLDivElement>(null) const scrollTargetRef = useRef<HTMLDivElement>(null)
const { fetchMods } = useNDKContext() const { fetchMods } = useNDKContext()
const [isFetching, setIsFetching] = useState(false) const [isFetching, setIsFetching] = useState(false)
const [mods, setMods] = useState<ModDetails[]>([]) const [mods, setMods] = useState<ModDetails[]>([])
const [filterOptions, setFilterOptions] = useState<FilterOptions>({
sort: SortBy.Latest, const [filterOptions] = useLocalStorage<FilterOptions>(
nsfw: NSFWFilter.Hide_NSFW, 'filter',
source: window.location.host, DEFAULT_FILTER_OPTIONS
moderated: ModeratedFilter.Moderated )
})
const muteLists = useMuteLists() const muteLists = useMuteLists()
const nsfwList = useNSFWList() const nsfwList = useNSFWList()
@ -112,10 +107,7 @@ export const ModsPage = () => {
ref={scrollTargetRef} ref={scrollTargetRef}
> >
<PageTitleRow /> <PageTitleRow />
<ModFilter <ModFilter />
filterOptions={filterOptions}
setFilterOptions={setFilterOptions}
/>
<div className='IBMSecMain IBMSMListWrapper'> <div className='IBMSecMain IBMSMListWrapper'>
<div className='IBMSMList'> <div className='IBMSMList'>
@ -146,8 +138,8 @@ const PageTitleRow = React.memo(() => {
const value = searchTermRef.current?.value || '' // Access the input value from the ref const value = searchTermRef.current?.value || '' // Access the input value from the ref
if (value !== '') { if (value !== '') {
const searchParams = createSearchParams({ const searchParams = createSearchParams({
searchTerm: value, q: value,
searching: 'Mods' kind: 'Mods'
}) })
navigate({ pathname: appRoutes.search, search: `?${searchParams}` }) navigate({ pathname: appRoutes.search, search: `?${searchParams}` })
} }
@ -166,35 +158,11 @@ const PageTitleRow = React.memo(() => {
<div className='IBMSMTitleMain'> <div className='IBMSMTitleMain'>
<h2 className='IBMSMTitleMainHeading'>Mods</h2> <h2 className='IBMSMTitleMainHeading'>Mods</h2>
</div> </div>
<div className='SearchMain'> <SearchInput
<div className='SearchMainInside'>
<div className='SearchMainInsideWrapper'>
<input
type='text'
className='SMIWInput'
ref={searchTermRef} ref={searchTermRef}
onKeyDown={handleKeyDown} handleKeyDown={handleKeyDown}
placeholder='Enter search term' handleSearch={handleSearch}
/> />
<button
className='btn btnMain SMIWButton'
type='button'
onClick={handleSearch}
>
<svg
xmlns='http://www.w3.org/2000/svg'
viewBox='0 0 512 512'
width='1em'
height='1em'
fill='currentColor'
>
<path d='M500.3 443.7l-119.7-119.7c27.22-40.41 40.65-90.9 33.46-144.7C401.8 87.79 326.8 13.32 235.2 1.723C99.01-15.51-15.51 99.01 1.724 235.2c11.6 91.64 86.08 166.7 177.6 178.9c53.8 7.189 104.3-6.236 144.7-33.46l119.7 119.7c15.62 15.62 40.95 15.62 56.57 0C515.9 484.7 515.9 459.3 500.3 443.7zM79.1 208c0-70.58 57.42-128 128-128s128 57.42 128 128c0 70.58-57.42 128-128 128S79.1 278.6 79.1 208z'></path>
</svg>
</button>
</div>
</div>
</div>
</div> </div>
</div> </div>
) )

View File

@ -8,8 +8,8 @@ import { Tabs } from 'components/Tabs'
import { MOD_FILTER_LIMIT } from '../constants' import { MOD_FILTER_LIMIT } from '../constants'
import { import {
useAppSelector, useAppSelector,
useDidMount,
useFilteredMods, useFilteredMods,
useLocalStorage,
useMuteLists, useMuteLists,
useNDKContext, useNDKContext,
useNSFWList useNSFWList
@ -19,17 +19,12 @@ import { useCallback, useEffect, useRef, useState } from 'react'
import { useParams, Navigate, Link } from 'react-router-dom' import { useParams, Navigate, Link } from 'react-router-dom'
import { toast } from 'react-toastify' import { toast } from 'react-toastify'
import { appRoutes, getProfilePageRoute } from 'routes' import { appRoutes, getProfilePageRoute } from 'routes'
import { import { FilterOptions, ModDetails, UserRelaysType } from 'types'
FilterOptions,
ModDetails,
ModeratedFilter,
NSFWFilter,
SortBy,
UserProfile,
UserRelaysType
} from 'types'
import { import {
copyTextToClipboard, copyTextToClipboard,
DEFAULT_FILTER_OPTIONS,
log,
LogType,
now, now,
npubToHex, npubToHex,
scrollIntoView, scrollIntoView,
@ -37,6 +32,7 @@ import {
signAndPublish signAndPublish
} from 'utils' } from 'utils'
import { CheckboxField } from 'components/Inputs' import { CheckboxField } from 'components/Inputs'
import { useProfile } from 'hooks/useProfile'
export const ProfilePage = () => { export const ProfilePage = () => {
// Try to decode nprofile parameter // Try to decode nprofile parameter
@ -48,27 +44,19 @@ export const ProfilePage = () => {
: undefined : undefined
profilePubkey = value?.data.pubkey profilePubkey = value?.data.pubkey
} catch (error) { } catch (error) {
// Failed to decode the nprofile
// Silently ignore and redirect to home or logged in user // Silently ignore and redirect to home or logged in user
log(true, LogType.Error, 'Failed to decode nprofile.', error)
} }
const scrollTargetRef = useRef<HTMLDivElement>(null) const scrollTargetRef = useRef<HTMLDivElement>(null)
const { ndk, publish, findMetadata, fetchEventFromUserRelays, fetchMods } = const { ndk, publish, fetchEventFromUserRelays, fetchMods } = useNDKContext()
useNDKContext()
const [profile, setProfile] = useState<UserProfile>()
const userState = useAppSelector((state) => state.user) const userState = useAppSelector((state) => state.user)
const isOwnProfile = const isOwnProfile =
userState.auth && userState.user?.pubkey === profilePubkey userState.auth && userState.user?.pubkey === profilePubkey
const [isLoading, setIsLoading] = useState(false) const [isLoading, setIsLoading] = useState(false)
const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('') const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('')
useDidMount(() => { const profile = useProfile(profilePubkey)
if (profilePubkey) {
findMetadata(profilePubkey).then((res) => {
setProfile(res)
})
}
})
const displayName = const displayName =
profile?.displayName || profile?.name || '[name not set up]' profile?.displayName || profile?.name || '[name not set up]'
@ -221,7 +209,7 @@ export const ProfilePage = () => {
kind: NDKKind.MuteList, kind: NDKKind.MuteList,
content: muteListEvent.content, content: muteListEvent.content,
created_at: now(), created_at: now(),
tags: tags.filter((item) => item[0] !== 'a' || item[1] !== profilePubkey) tags: tags.filter((item) => item[0] !== 'p' || item[1] !== profilePubkey)
} }
setLoadingSpinnerDesc('Updating mute list event') setLoadingSpinnerDesc('Updating mute list event')
@ -239,12 +227,9 @@ export const ProfilePage = () => {
// Mods // Mods
const [mods, setMods] = useState<ModDetails[]>([]) const [mods, setMods] = useState<ModDetails[]>([])
const [filterOptions, setFilterOptions] = useState<FilterOptions>({ const filterKey = 'filter-profile'
sort: SortBy.Latest, const [filterOptions] = useLocalStorage<FilterOptions>(filterKey, {
nsfw: NSFWFilter.Hide_NSFW, ...DEFAULT_FILTER_OPTIONS
source: window.location.host,
moderated: ModeratedFilter.Moderated,
author: profilePubkey
}) })
const muteLists = useMuteLists() const muteLists = useMuteLists()
const nsfwList = useNSFWList() const nsfwList = useNSFWList()
@ -313,7 +298,8 @@ export const ProfilePage = () => {
userState, userState,
filterOptions, filterOptions,
nsfwList, nsfwList,
muteLists muteLists,
profilePubkey
) )
// Redirect route // Redirect route
@ -479,10 +465,7 @@ export const ProfilePage = () => {
{/* Tabs Content */} {/* Tabs Content */}
{tab === 0 && ( {tab === 0 && (
<> <>
<ModFilter <ModFilter filterKey={filterKey} author={profilePubkey} />
filterOptions={filterOptions}
setFilterOptions={setFilterOptions}
/>
<div className='IBMSMList IBMSMListAlt'> <div className='IBMSMList IBMSMListAlt'>
{filteredModList.map((mod) => ( {filteredModList.map((mod) => (

View File

@ -4,15 +4,16 @@ import {
NDKKind, NDKKind,
NDKSubscriptionCacheUsage, NDKSubscriptionCacheUsage,
NDKUserProfile, NDKUserProfile,
NostrEvent,
profileFromEvent profileFromEvent
} from '@nostr-dev-kit/ndk' } from '@nostr-dev-kit/ndk'
import { ErrorBoundary } from 'components/ErrorBoundary' import { ErrorBoundary } from 'components/ErrorBoundary'
import { GameCard } from 'components/GameCard' import { GameCard } from 'components/GameCard'
import { LoadingSpinner } from 'components/LoadingSpinner'
import { ModCard } from 'components/ModCard' import { ModCard } from 'components/ModCard'
import { ModFilter } from 'components/ModsFilter' import { ModFilter } from 'components/ModsFilter'
import { Pagination } from 'components/Pagination' import { Pagination } from 'components/Pagination'
import { Profile } from 'components/ProfileSection' import { Profile } from 'components/ProfileSection'
import { SearchInput } from 'components/SearchInput'
import { import {
MAX_GAMES_PER_PAGE, MAX_GAMES_PER_PAGE,
MAX_MODS_PER_PAGE, MAX_MODS_PER_PAGE,
@ -22,32 +23,18 @@ import {
useAppSelector, useAppSelector,
useFilteredMods, useFilteredMods,
useGames, useGames,
useLocalStorage,
useMuteLists, useMuteLists,
useNDKContext, useNDKContext,
useNSFWList useNSFWList
} from 'hooks' } from 'hooks'
import React, { import React, { useEffect, useMemo, useRef, useState } from 'react'
Dispatch,
SetStateAction,
useEffect,
useMemo,
useRef,
useState
} from 'react'
import { useSearchParams } from 'react-router-dom' import { useSearchParams } from 'react-router-dom'
import { FilterOptions, ModDetails, ModeratedFilter, MuteLists } from 'types'
import { import {
FilterOptions, DEFAULT_FILTER_OPTIONS,
ModDetails,
ModeratedFilter,
MuteLists,
NSFWFilter,
SortBy
} from 'types'
import {
extractModData, extractModData,
isModDataComplete, isModDataComplete,
log,
LogType,
scrollIntoView scrollIntoView
} from 'utils' } from 'utils'
@ -59,30 +46,35 @@ enum SearchKindEnum {
export const SearchPage = () => { export const SearchPage = () => {
const scrollTargetRef = useRef<HTMLDivElement>(null) const scrollTargetRef = useRef<HTMLDivElement>(null)
const [searchParams] = useSearchParams() const [searchParams, setSearchParams] = useSearchParams()
const muteLists = useMuteLists() const muteLists = useMuteLists()
const nsfwList = useNSFWList() const nsfwList = useNSFWList()
const searchTermRef = useRef<HTMLInputElement>(null) const searchTermRef = useRef<HTMLInputElement>(null)
const [searchKind, setSearchKind] = useState( const searchKind =
(searchParams.get('searching') as SearchKindEnum) || SearchKindEnum.Mods (searchParams.get('kind') as SearchKindEnum) || SearchKindEnum.Mods
const [filterOptions] = useLocalStorage<FilterOptions>(
'filter',
DEFAULT_FILTER_OPTIONS
) )
const [filterOptions, setFilterOptions] = useState<FilterOptions>({ const [searchTerm, setSearchTerm] = useState(searchParams.get('q') || '')
sort: SortBy.Latest,
nsfw: NSFWFilter.Hide_NSFW,
source: window.location.host,
moderated: ModeratedFilter.Moderated
})
const [searchTerm, setSearchTerm] = useState(
searchParams.get('searchTerm') || ''
)
const handleSearch = () => { const handleSearch = () => {
const value = searchTermRef.current?.value || '' // Access the input value from the ref const value = searchTermRef.current?.value || '' // Access the input value from the ref
setSearchTerm(value) setSearchTerm(value)
if (value) {
searchParams.set('q', value)
} else {
searchParams.delete('q')
}
setSearchParams(searchParams, {
replace: true
})
} }
// Handle "Enter" key press inside the input // Handle "Enter" key press inside the input
@ -109,42 +101,14 @@ export const SearchPage = () => {
</span> </span>
</h2> </h2>
</div> </div>
<div className='SearchMain'> <SearchInput
<div className='SearchMainInside'> handleKeyDown={handleKeyDown}
<div className='SearchMainInsideWrapper'> handleSearch={handleSearch}
<input
type='text'
className='SMIWInput'
ref={searchTermRef} ref={searchTermRef}
onKeyDown={handleKeyDown}
placeholder='Enter search term'
/> />
<button
className='btn btnMain SMIWButton'
type='button'
onClick={handleSearch}
>
<svg
xmlns='http://www.w3.org/2000/svg'
viewBox='0 0 512 512'
width='1em'
height='1em'
fill='currentColor'
>
<path d='M500.3 443.7l-119.7-119.7c27.22-40.41 40.65-90.9 33.46-144.7C401.8 87.79 326.8 13.32 235.2 1.723C99.01-15.51-15.51 99.01 1.724 235.2c11.6 91.64 86.08 166.7 177.6 178.9c53.8 7.189 104.3-6.236 144.7-33.46l119.7 119.7c15.62 15.62 40.95 15.62 56.57 0C515.9 484.7 515.9 459.3 500.3 443.7zM79.1 208c0-70.58 57.42-128 128-128s128 57.42 128 128c0 70.58-57.42 128-128 128S79.1 278.6 79.1 208z'></path>
</svg>
</button>
</div> </div>
</div> </div>
</div> <Filters />
</div>
</div>
<Filters
filterOptions={filterOptions}
setFilterOptions={setFilterOptions}
searchKind={searchKind}
setSearchKind={setSearchKind}
/>
{searchKind === SearchKindEnum.Mods && ( {searchKind === SearchKindEnum.Mods && (
<ModsResult <ModsResult
searchTerm={searchTerm} searchTerm={searchTerm}
@ -170,31 +134,27 @@ export const SearchPage = () => {
) )
} }
type FiltersProps = { const Filters = React.memo(() => {
filterOptions: FilterOptions const [filterOptions, setFilterOptions] = useLocalStorage<FilterOptions>(
setFilterOptions: Dispatch<SetStateAction<FilterOptions>> 'filter',
searchKind: SearchKindEnum DEFAULT_FILTER_OPTIONS
setSearchKind: Dispatch<SetStateAction<SearchKindEnum>> )
}
const Filters = React.memo(
({
filterOptions,
setFilterOptions,
searchKind,
setSearchKind
}: FiltersProps) => {
const userState = useAppSelector((state) => state.user) const userState = useAppSelector((state) => state.user)
const [searchParams, setSearchParams] = useSearchParams()
const searchKind =
(searchParams.get('kind') as SearchKindEnum) || SearchKindEnum.Mods
const handleChangeSearchKind = (kind: SearchKindEnum) => {
searchParams.set('kind', kind)
setSearchParams(searchParams, {
replace: true
})
}
return ( return (
<div className='IBMSecMain'> <div className='IBMSecMain'>
<div className='FiltersMain'> <div className='FiltersMain'>
{searchKind === SearchKindEnum.Mods && ( {searchKind === SearchKindEnum.Mods && <ModFilter />}
<ModFilter
filterOptions={filterOptions}
setFilterOptions={setFilterOptions}
/>
)}
{searchKind === SearchKindEnum.Users && ( {searchKind === SearchKindEnum.Users && (
<div className='FiltersMainElement'> <div className='FiltersMainElement'>
@ -252,7 +212,7 @@ const Filters = React.memo(
<div <div
key={`searchingFilterItem-${index}`} key={`searchingFilterItem-${index}`}
className='dropdown-item dropdownMainMenuItem' className='dropdown-item dropdownMainMenuItem'
onClick={() => setSearchKind(item)} onClick={() => handleChangeSearchKind(item)}
> >
{item} {item}
</div> </div>
@ -263,8 +223,7 @@ const Filters = React.memo(
</div> </div>
</div> </div>
) )
} })
)
type ModsResultProps = { type ModsResultProps = {
filterOptions: FilterOptions filterOptions: FilterOptions
@ -324,6 +283,7 @@ const ModsResult = ({
}, [searchTerm]) }, [searchTerm])
const filteredMods = useMemo(() => { const filteredMods = useMemo(() => {
// Search page requires search term
if (searchTerm === '') return [] if (searchTerm === '') return []
const lowerCaseSearchTerm = searchTerm.toLowerCase() const lowerCaseSearchTerm = searchTerm.toLowerCase()
@ -337,8 +297,16 @@ const ModsResult = ({
tag.toLowerCase().includes(lowerCaseSearchTerm) tag.toLowerCase().includes(lowerCaseSearchTerm)
) > -1 ) > -1
return mods.filter(filterFn) const filterSourceFn = (mod: ModDetails) => {
}, [mods, searchTerm]) // Filter by source if selected
if (filterOptions.source === window.location.host) {
return mod.rTag === filterOptions.source
}
return true
}
return mods.filter(filterFn).filter(filterSourceFn)
}, [filterOptions.source, mods, searchTerm])
const filteredModList = useFilteredMods( const filteredModList = useFilteredMods(
filteredMods, filteredMods,
@ -393,39 +361,70 @@ const UsersResult = ({
moderationFilter, moderationFilter,
muteLists muteLists
}: UsersResultProps) => { }: UsersResultProps) => {
const { fetchEvents } = useNDKContext() const { ndk } = useNDKContext()
const [isFetching, setIsFetching] = useState(false)
const [profiles, setProfiles] = useState<NDKUserProfile[]>([]) const [profiles, setProfiles] = useState<NDKUserProfile[]>([])
const userState = useAppSelector((state) => state.user) const userState = useAppSelector((state) => state.user)
useEffect(() => { useEffect(() => {
if (searchTerm === '') { if (searchTerm === '') {
setProfiles([]) setProfiles([])
} else { } else {
const filter: NDKFilter = { const sub = ndk.subscribe(
{
kinds: [NDKKind.Metadata], kinds: [NDKKind.Metadata],
search: searchTerm search: searchTerm
},
{
closeOnEose: true,
cacheUsage: NDKSubscriptionCacheUsage.PARALLEL
},
undefined,
false
)
// Stop the sub after 10 seconds if we are still searching the same term as before
window.setTimeout(() => {
if (sub.filter.search === searchTerm) {
sub.stop()
}
}, 10000)
const onEvent = (event: NostrEvent | NDKEvent) => {
if (!(event instanceof NDKEvent)) event = new NDKEvent(undefined, event)
const dedupKey = event.deduplicationKey()
const existingEvent = events.get(dedupKey)
if (existingEvent) {
event = dedup(existingEvent, event)
}
event.ndk = this
events.set(dedupKey, event)
// We can't rely on the 'eose' to arrive
// Instead we repeat and sort results on each event
const ndkEvents = Array.from(events.values())
const profiles: NDKUserProfile[] = []
ndkEvents.forEach((event) => {
try {
const profile = profileFromEvent(event)
profiles.push(profile)
} catch (error) {
// If we are unable to parse silently skip over the errors
}
})
setProfiles(profiles)
} }
setIsFetching(true) // Clear previous results
fetchEvents(filter) const events = new Map<string, NDKEvent>()
.then((events) => {
const results = events.map((event) => { // Bind handler and start the sub
const ndkEvent = new NDKEvent(undefined, event) sub.on('event', onEvent)
const profile = profileFromEvent(ndkEvent) sub.start()
return profile return () => {
}) sub.stop()
setProfiles(results)
})
.catch((err) => {
log(true, LogType.Error, 'An error occurred in fetching users', err)
})
.finally(() => {
setIsFetching(false)
})
} }
}, [searchTerm, fetchEvents]) }
}, [ndk, searchTerm])
const filteredProfiles = useMemo(() => { const filteredProfiles = useMemo(() => {
let filtered = [...profiles] let filtered = [...profiles]
@ -450,25 +449,13 @@ const UsersResult = ({
}, [userState.user?.npub, moderationFilter, profiles, muteLists]) }, [userState.user?.npub, moderationFilter, profiles, muteLists])
return ( return (
<> <>
{isFetching && <LoadingSpinner desc='Fetching Profiles' />}
<div className='IBMSecMain IBMSMListWrapper'> <div className='IBMSecMain IBMSMListWrapper'>
<div className='IBMSMList'> <div className='IBMSMList'>
{filteredProfiles.map((profile) => { {filteredProfiles.map((profile) => {
if (profile.pubkey) { if (profile.pubkey) {
const displayName =
profile?.displayName || profile?.name || '[name not set up]'
const about = profile?.bio || profile?.about || '[bio not set up]'
return ( return (
<ErrorBoundary key={profile.pubkey}> <ErrorBoundary key={profile.pubkey}>
<Profile <Profile pubkey={profile.pubkey as string} />
pubkey={profile.pubkey as string}
displayName={displayName}
about={about}
image={profile?.image}
nip05={profile?.nip05}
lud16={profile?.lud16}
/>
</ErrorBoundary> </ErrorBoundary>
) )
} }
@ -536,3 +523,11 @@ const GamesResult = ({ searchTerm }: GamesResultProps) => {
</> </>
) )
} }
function dedup(event1: NDKEvent, event2: NDKEvent) {
// return the newest of the two
if (event1.created_at! > event2.created_at!) {
return event1
}
return event2
}

View File

@ -98,15 +98,15 @@ export const ProfileSettings = () => {
// In case user is not logged in clicking on profile link will navigate to homepage // In case user is not logged in clicking on profile link will navigate to homepage
let profileRoute = appRoutes.home let profileRoute = appRoutes.home
let nprofile: string | undefined
if (userState.auth && userState.user) { if (userState.auth && userState.user) {
const hexPubkey = npubToHex(userState.user.npub as string) const hexPubkey = npubToHex(userState.user.npub as string)
if (hexPubkey) { if (hexPubkey) {
profileRoute = getProfilePageRoute( nprofile = nip19.nprofileEncode({
nip19.nprofileEncode({
pubkey: hexPubkey pubkey: hexPubkey
}) })
) profileRoute = getProfilePageRoute(nprofile)
} }
} }
@ -247,10 +247,8 @@ export const ProfileSettings = () => {
<path d='M384 96L384 0h-112c-26.51 0-48 21.49-48 48v288c0 26.51 21.49 48 48 48H464c26.51 0 48-21.49 48-48V128h-95.1C398.4 128 384 113.6 384 96zM416 0v96h96L416 0zM192 352V128h-144c-26.51 0-48 21.49-48 48v288c0 26.51 21.49 48 48 48h192c26.51 0 48-21.49 48-48L288 416h-32C220.7 416 192 387.3 192 352z'></path> <path d='M384 96L384 0h-112c-26.51 0-48 21.49-48 48v288c0 26.51 21.49 48 48 48H464c26.51 0 48-21.49 48-48V128h-95.1C398.4 128 384 113.6 384 96zM416 0v96h96L416 0zM192 352V128h-144c-26.51 0-48 21.49-48 48v288c0 26.51 21.49 48 48 48h192c26.51 0 48-21.49 48-48L288 416h-32C220.7 416 192 387.3 192 352z'></path>
</svg> </svg>
</div> </div>
{typeof userState.user?.pubkey === 'string' && ( {typeof nprofile !== 'undefined' && (
<ProfileQRButtonWithPopUp <ProfileQRButtonWithPopUp nprofile={nprofile} />
pubkey={userState.user.pubkey}
/>
)} )}
</div> </div>
</div> </div>

View File

@ -155,3 +155,12 @@
backdrop-filter: blur(10px); backdrop-filter: blur(10px);
background: rgba(35, 35, 35, 0.85); background: rgba(35, 35, 35, 0.85);
} }
.IBMSMSMBSSTagsTag.IBMSMSMBSSTagsTagRepost.IBMSMSMBSSTagsTagRepostCard {
position: absolute;
bottom: 10px;
left: 10px;
-webkit-backdrop-filter: blur(10px);
backdrop-filter: blur(10px);
background: #232323d9;
}

View File

@ -44,3 +44,13 @@
cursor: default; cursor: default;
box-shadow: unset; box-shadow: unset;
} }
.IBMSMSMBSSTagsTag.IBMSMSMBSSTagsTagRepost {
background: #ffffff1a;
color: #ffffff59;
font-weight: 700;
border: unset;
font-size: 14px;
cursor: default;
box-shadow: unset;
}

View File

@ -22,5 +22,4 @@ export interface FilterOptions {
nsfw: NSFWFilter nsfw: NSFWFilter
source: string source: string
moderated: ModeratedFilter moderated: ModeratedFilter
author?: string
} }

8
src/utils/consts.ts Normal file
View File

@ -0,0 +1,8 @@
import { FilterOptions, SortBy, NSFWFilter, ModeratedFilter } from 'types'
export const DEFAULT_FILTER_OPTIONS: FilterOptions = {
sort: SortBy.Latest,
nsfw: NSFWFilter.Hide_NSFW,
source: window.location.host,
moderated: ModeratedFilter.Moderated
}

View File

@ -3,3 +3,5 @@ export * from './nostr'
export * from './url' export * from './url'
export * from './utils' export * from './utils'
export * from './zap' export * from './zap'
export * from './localStorage'
export * from './consts'

32
src/utils/localStorage.ts Normal file
View File

@ -0,0 +1,32 @@
export function getLocalStorageItem<T>(key: string, defaultValue: T): string {
try {
const data = window.localStorage.getItem(key)
if (data === null) return JSON.stringify(defaultValue)
return data
} catch (err) {
console.error(`Error while fetching local storage value: `, err)
return JSON.stringify(defaultValue)
}
}
export function setLocalStorageItem(key: string, value: string) {
try {
window.localStorage.setItem(key, value)
dispatchLocalStorageEvent(key, value)
} catch (err) {
console.error(`Error while saving local storage value: `, err)
}
}
export function removeLocalStorageItem(key: string) {
try {
window.localStorage.removeItem(key)
dispatchLocalStorageEvent(key, null)
} catch (err) {
console.error(`Error while deleting local storage value: `, err)
}
}
function dispatchLocalStorageEvent(key: string, newValue: string | null) {
window.dispatchEvent(new StorageEvent('storage', { key, newValue }))
}