WoT implemented, plus other fixes #160
@ -7,6 +7,9 @@ VITE_ADMIN_NPUBS= <A comma separated list of npubs>
|
|||||||
# A dedicated npub used for reporting mods, blogs, profile and etc.
|
# A dedicated npub used for reporting mods, blogs, profile and etc.
|
||||||
VITE_REPORTING_NPUB= <npub1...>
|
VITE_REPORTING_NPUB= <npub1...>
|
||||||
|
|
||||||
|
# A dedicated npub used for site WOT.
|
||||||
|
VITE_SITE_WOT_NPUB= <npub1...>
|
||||||
|
|
||||||
# if there's no featured image, or if the image breaks somewhere down the line, then it should default to this image
|
# if there's no featured image, or if the image breaks somewhere down the line, then it should default to this image
|
||||||
VITE_FALLBACK_MOD_IMAGE=https://image.nostr.build/1fa7afd3489302c2da8957993ac0fd6c4308eedd4b1b95b24ecfabe3651b2183.png
|
VITE_FALLBACK_MOD_IMAGE=https://image.nostr.build/1fa7afd3489302c2da8957993ac0fd6c4308eedd4b1b95b24ecfabe3651b2183.png
|
||||||
|
|
||||||
|
@ -25,6 +25,7 @@ jobs:
|
|||||||
echo "VITE_APP_RELAY=${{ vars.VITE_APP_RELAY }}" >> .env
|
echo "VITE_APP_RELAY=${{ vars.VITE_APP_RELAY }}" >> .env
|
||||||
echo "VITE_ADMIN_NPUBS=${{ vars.VITE_ADMIN_NPUBS }}" >> .env
|
echo "VITE_ADMIN_NPUBS=${{ vars.VITE_ADMIN_NPUBS }}" >> .env
|
||||||
echo "VITE_REPORTING_NPUB=${{ vars.VITE_REPORTING_NPUB }}" >> .env
|
echo "VITE_REPORTING_NPUB=${{ vars.VITE_REPORTING_NPUB }}" >> .env
|
||||||
|
echo "VITE_SITE_WOT_NPUB"=${{ vars.VITE_SITE_WOT_NPUB }}" >> .env
|
||||||
echo "VITE_FALLBACK_MOD_IMAGE=${{ vars.VITE_FALLBACK_MOD_IMAGE }}" >> .env
|
echo "VITE_FALLBACK_MOD_IMAGE=${{ vars.VITE_FALLBACK_MOD_IMAGE }}" >> .env
|
||||||
echo "VITE_FALLBACK_GAME_IMAGE=${{ vars.VITE_FALLBACK_GAME_IMAGE }}" >> .env
|
echo "VITE_FALLBACK_GAME_IMAGE=${{ vars.VITE_FALLBACK_GAME_IMAGE }}" >> .env
|
||||||
echo "VITE_BLOG_NPUBS=${{ vars.VITE_BLOG_NPUBS }}" >> .env
|
echo "VITE_BLOG_NPUBS=${{ vars.VITE_BLOG_NPUBS }}" >> .env
|
||||||
|
@ -25,6 +25,7 @@ jobs:
|
|||||||
echo "VITE_APP_RELAY=${{ vars.VITE_APP_RELAY }}" >> .env
|
echo "VITE_APP_RELAY=${{ vars.VITE_APP_RELAY }}" >> .env
|
||||||
echo "VITE_ADMIN_NPUBS=${{ vars.VITE_ADMIN_NPUBS }}" >> .env
|
echo "VITE_ADMIN_NPUBS=${{ vars.VITE_ADMIN_NPUBS }}" >> .env
|
||||||
echo "VITE_REPORTING_NPUB=${{ vars.VITE_REPORTING_NPUB }}" >> .env
|
echo "VITE_REPORTING_NPUB=${{ vars.VITE_REPORTING_NPUB }}" >> .env
|
||||||
|
echo "VITE_SITE_WOT_NPUB"=${{ vars.VITE_SITE_WOT_NPUB }}" >> .env
|
||||||
echo "VITE_FALLBACK_MOD_IMAGE=${{ vars.VITE_FALLBACK_MOD_IMAGE }}" >> .env
|
echo "VITE_FALLBACK_MOD_IMAGE=${{ vars.VITE_FALLBACK_MOD_IMAGE }}" >> .env
|
||||||
echo "VITE_FALLBACK_GAME_IMAGE=${{ vars.VITE_FALLBACK_GAME_IMAGE }}" >> .env
|
echo "VITE_FALLBACK_GAME_IMAGE=${{ vars.VITE_FALLBACK_GAME_IMAGE }}" >> .env
|
||||||
echo "VITE_BLOG_NPUBS=${{ vars.VITE_BLOG_NPUBS }}" >> .env
|
echo "VITE_BLOG_NPUBS=${{ vars.VITE_BLOG_NPUBS }}" >> .env
|
||||||
|
@ -32,6 +32,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
echo "VITE_APP_RELAY=${{ vars.VITE_APP_RELAY }}" >> .env
|
echo "VITE_APP_RELAY=${{ vars.VITE_APP_RELAY }}" >> .env
|
||||||
echo "VITE_ADMIN_NPUBS=${{ vars.VITE_ADMIN_NPUBS }}" >> .env
|
echo "VITE_ADMIN_NPUBS=${{ vars.VITE_ADMIN_NPUBS }}" >> .env
|
||||||
|
echo "VITE_SITE_WOT_NPUB"=${{ vars.VITE_SITE_WOT_NPUB }}" >> .env
|
||||||
echo "VITE_FALLBACK_GAME_IMAGE=${{ vars.VITE_FALLBACK_GAME_IMAGE }}" >> .env
|
echo "VITE_FALLBACK_GAME_IMAGE=${{ vars.VITE_FALLBACK_GAME_IMAGE }}" >> .env
|
||||||
echo "VITE_FALLBACK_MOD_IMAGE=${{ vars.VITE_FALLBACK_MOD_IMAGE }}" >> .env
|
echo "VITE_FALLBACK_MOD_IMAGE=${{ vars.VITE_FALLBACK_MOD_IMAGE }}" >> .env
|
||||||
echo "VITE_REPORTING_NPUB=${{ vars.VITE_REPORTING_NPUB }}" >> .env
|
echo "VITE_REPORTING_NPUB=${{ vars.VITE_REPORTING_NPUB }}" >> .env
|
||||||
|
@ -1,6 +1,12 @@
|
|||||||
import { useAppSelector, useLocalStorage } from 'hooks'
|
import { useAppSelector, useLocalStorage } from 'hooks'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { FilterOptions, ModeratedFilter, NSFWFilter, SortBy } from 'types'
|
import {
|
||||||
|
FilterOptions,
|
||||||
|
ModeratedFilter,
|
||||||
|
NSFWFilter,
|
||||||
|
SortBy,
|
||||||
|
WOTFilterOptions
|
||||||
|
} from 'types'
|
||||||
import { DEFAULT_FILTER_OPTIONS } from 'utils'
|
import { DEFAULT_FILTER_OPTIONS } from 'utils'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@ -19,6 +25,7 @@ export const ModFilter = React.memo(
|
|||||||
return (
|
return (
|
||||||
<div className='IBMSecMain'>
|
<div className='IBMSecMain'>
|
||||||
<div className='FiltersMain'>
|
<div className='FiltersMain'>
|
||||||
|
{/* sort filter options */}
|
||||||
<div className='FiltersMainElement'>
|
<div className='FiltersMainElement'>
|
||||||
<div className='dropdown dropdownMain'>
|
<div className='dropdown dropdownMain'>
|
||||||
<button
|
<button
|
||||||
@ -48,6 +55,8 @@ export const ModFilter = React.memo(
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* moderation filter options */}
|
||||||
<div className='FiltersMainElement'>
|
<div className='FiltersMainElement'>
|
||||||
<div className='dropdown dropdownMain'>
|
<div className='dropdown dropdownMain'>
|
||||||
<button
|
<button
|
||||||
@ -91,6 +100,63 @@ export const ModFilter = React.memo(
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* wot filter options */}
|
||||||
|
<div className='FiltersMainElement'>
|
||||||
|
<div className='dropdown dropdownMain'>
|
||||||
|
<button
|
||||||
|
className='btn dropdown-toggle btnMain btnMainDropdown'
|
||||||
|
aria-expanded='false'
|
||||||
|
data-bs-toggle='dropdown'
|
||||||
|
type='button'
|
||||||
|
>
|
||||||
|
{filterOptions.wot}
|
||||||
|
</button>
|
||||||
|
<div className='dropdown-menu dropdownMainMenu'>
|
||||||
|
{Object.values(WOTFilterOptions).map((item, index) => {
|
||||||
|
// when user is not logged in
|
||||||
|
if (
|
||||||
|
(item === WOTFilterOptions.Site_And_Mine ||
|
||||||
|
item === WOTFilterOptions.Mine_Only) &&
|
||||||
|
!userState.auth
|
||||||
|
) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// when logged in user not admin
|
||||||
|
if (item === WOTFilterOptions.None) {
|
||||||
|
const isAdmin =
|
||||||
|
userState.user?.npub ===
|
||||||
|
import.meta.env.VITE_REPORTING_NPUB
|
||||||
|
|
||||||
|
const isOwnProfile =
|
||||||
|
author &&
|
||||||
|
userState.auth &&
|
||||||
|
userState.user?.pubkey === author
|
||||||
|
|
||||||
|
if (!(isAdmin || isOwnProfile)) return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={`wotFilterOption-${index}`}
|
||||||
|
className='dropdown-item dropdownMainMenuItem'
|
||||||
|
onClick={() =>
|
||||||
|
setFilterOptions((prev) => ({
|
||||||
|
...prev,
|
||||||
|
wot: item
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{item}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* nsfw filter options */}
|
||||||
<div className='FiltersMainElement'>
|
<div className='FiltersMainElement'>
|
||||||
<div className='dropdown dropdownMain'>
|
<div className='dropdown dropdownMain'>
|
||||||
<button
|
<button
|
||||||
@ -119,6 +185,8 @@ export const ModFilter = React.memo(
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* source filter options */}
|
||||||
<div className='FiltersMainElement'>
|
<div className='FiltersMainElement'>
|
||||||
<div className='dropdown dropdownMain'>
|
<div className='dropdown dropdownMain'>
|
||||||
<button
|
<button
|
||||||
|
@ -369,7 +369,7 @@ export const NDKContextProvider = ({ children }: { children: ReactNode }) => {
|
|||||||
if (!event.sig) throw new Error('Before publishing first sign the event!')
|
if (!event.sig) throw new Error('Before publishing first sign the event!')
|
||||||
|
|
||||||
return event
|
return event
|
||||||
.publish(undefined, 30000)
|
.publish(undefined, 10000)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
const relaysPublishedOn = Array.from(res)
|
const relaysPublishedOn = Array.from(res)
|
||||||
return relaysPublishedOn.map((relay) => relay.url)
|
return relaysPublishedOn.map((relay) => relay.url)
|
||||||
|
@ -6,9 +6,11 @@ import {
|
|||||||
ModeratedFilter,
|
ModeratedFilter,
|
||||||
MuteLists,
|
MuteLists,
|
||||||
NSFWFilter,
|
NSFWFilter,
|
||||||
SortBy
|
SortBy,
|
||||||
|
WOTFilterOptions
|
||||||
} from 'types'
|
} from 'types'
|
||||||
import { npubToHex } from 'utils'
|
import { npubToHex } from 'utils'
|
||||||
|
import { useAppSelector } from './redux'
|
||||||
|
|
||||||
export const useFilteredMods = (
|
export const useFilteredMods = (
|
||||||
mods: ModDetails[],
|
mods: ModDetails[],
|
||||||
@ -21,6 +23,8 @@ export const useFilteredMods = (
|
|||||||
},
|
},
|
||||||
author?: string | undefined
|
author?: string | undefined
|
||||||
) => {
|
) => {
|
||||||
|
const { siteWot, userWot } = useAppSelector((state) => state.wot)
|
||||||
|
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
const nsfwFilter = (mods: ModDetails[]) => {
|
const nsfwFilter = (mods: ModDetails[]) => {
|
||||||
// Add nsfw tag to mods included in nsfwList
|
// Add nsfw tag to mods included in nsfwList
|
||||||
@ -46,8 +50,27 @@ export const useFilteredMods = (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const wotFilter = (mods: ModDetails[]) => {
|
||||||
|
// Determine the filtering logic based on the WOT filter option
|
||||||
|
switch (filterOptions.wot) {
|
||||||
|
case WOTFilterOptions.None:
|
||||||
|
return mods
|
||||||
|
case WOTFilterOptions.Site_Only:
|
||||||
|
return mods.filter((mod) => siteWot.includes(mod.author))
|
||||||
|
case WOTFilterOptions.Mine_Only:
|
||||||
|
return mods.filter((mod) => userWot.includes(mod.author))
|
||||||
|
case WOTFilterOptions.Site_And_Mine:
|
||||||
|
return mods.filter(
|
||||||
|
(mod) =>
|
||||||
|
siteWot.includes(mod.author) || userWot.includes(mod.author)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let filtered = nsfwFilter(mods)
|
let filtered = nsfwFilter(mods)
|
||||||
|
|
||||||
|
filtered = wotFilter(filtered)
|
||||||
|
|
||||||
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 &&
|
||||||
@ -84,10 +107,13 @@ export const useFilteredMods = (
|
|||||||
userState.user?.npub,
|
userState.user?.npub,
|
||||||
filterOptions.sort,
|
filterOptions.sort,
|
||||||
filterOptions.moderated,
|
filterOptions.moderated,
|
||||||
|
filterOptions.wot,
|
||||||
filterOptions.nsfw,
|
filterOptions.nsfw,
|
||||||
author,
|
author,
|
||||||
mods,
|
mods,
|
||||||
muteLists,
|
muteLists,
|
||||||
nsfwList
|
nsfwList,
|
||||||
|
siteWot,
|
||||||
|
userWot
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
@ -21,6 +21,7 @@ import '../styles/popup.css'
|
|||||||
import { npubToHex } from '../utils'
|
import { npubToHex } from '../utils'
|
||||||
import logo from '../assets/img/DEG Mods Logo With Text.svg'
|
import logo from '../assets/img/DEG Mods Logo With Text.svg'
|
||||||
import placeholder from '../assets/img/DEG Mods Default PP.png'
|
import placeholder from '../assets/img/DEG Mods Default PP.png'
|
||||||
|
import { setUserWot } from 'store/reducers/wot'
|
||||||
|
|
||||||
export const Header = () => {
|
export const Header = () => {
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
@ -48,6 +49,7 @@ export const Header = () => {
|
|||||||
if (opts.type === 'logout') {
|
if (opts.type === 'logout') {
|
||||||
dispatch(setAuth(null))
|
dispatch(setAuth(null))
|
||||||
dispatch(setUser(null))
|
dispatch(setUser(null))
|
||||||
|
dispatch(setUserWot([]))
|
||||||
} else {
|
} else {
|
||||||
dispatch(
|
dispatch(
|
||||||
setAuth({
|
setAuth({
|
||||||
|
@ -3,13 +3,123 @@ import { Footer } from './footer'
|
|||||||
import { Header } from './header'
|
import { Header } from './header'
|
||||||
import { SocialNav } from './socialNav'
|
import { SocialNav } from './socialNav'
|
||||||
import { Head } from './head'
|
import { Head } from './head'
|
||||||
|
import { useAppDispatch, useAppSelector, useNDKContext } from 'hooks'
|
||||||
|
import { useEffect } from 'react'
|
||||||
|
import { npubToHex } from 'utils'
|
||||||
|
import { calculateWot } from 'utils/wot'
|
||||||
|
import {
|
||||||
|
setSiteWot,
|
||||||
|
setSiteWotLevel,
|
||||||
|
setSiteWotStatus,
|
||||||
|
setUserWot,
|
||||||
|
setUserWotLevel,
|
||||||
|
WOTStatus
|
||||||
|
} from 'store/reducers/wot'
|
||||||
|
import { toast } from 'react-toastify'
|
||||||
|
import { LoadingSpinner } from 'components/LoadingSpinner'
|
||||||
|
import { NDKKind } from '@nostr-dev-kit/ndk'
|
||||||
|
import { UserRelaysType } from 'types'
|
||||||
|
|
||||||
export const Layout = () => {
|
export const Layout = () => {
|
||||||
|
const dispatch = useAppDispatch()
|
||||||
|
const { ndk, fetchEventFromUserRelays } = useNDKContext()
|
||||||
|
const userState = useAppSelector((state) => state.user)
|
||||||
|
const { siteWotStatus, siteWotLevel, userWotLevel } = useAppSelector(
|
||||||
|
(state) => state.wot
|
||||||
|
)
|
||||||
|
|
||||||
|
// calculate site's wot
|
||||||
|
useEffect(() => {
|
||||||
|
if (ndk) {
|
||||||
|
const SITE_WOT_NPUB = import.meta.env.VITE_SITE_WOT_NPUB
|
||||||
|
const hexPubkey = npubToHex(SITE_WOT_NPUB)
|
||||||
|
if (hexPubkey) {
|
||||||
|
dispatch(setSiteWotStatus(WOTStatus.LOADING))
|
||||||
|
calculateWot(hexPubkey, ndk, siteWotLevel)
|
||||||
|
.then((wot) => {
|
||||||
|
dispatch(setSiteWot(Array.from(wot)))
|
||||||
|
dispatch(setSiteWotStatus(WOTStatus.LOADED))
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.trace('An error occurred in calculating site WOT', err)
|
||||||
|
toast.error('An error occurred in calculating site web-of-trust!')
|
||||||
|
dispatch(setSiteWotStatus(WOTStatus.FAILED))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [ndk, siteWotLevel, dispatch])
|
||||||
|
|
||||||
|
// calculate user's wot
|
||||||
|
useEffect(() => {
|
||||||
|
if (ndk && userState.user?.pubkey) {
|
||||||
|
const hexPubkey = npubToHex(userState.user.pubkey as string)
|
||||||
|
if (hexPubkey)
|
||||||
|
calculateWot(hexPubkey, ndk, userWotLevel)
|
||||||
|
.then((wot) => {
|
||||||
|
dispatch(setUserWot(Array.from(wot)))
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.trace('An error occurred in calculating user WOT', err)
|
||||||
|
toast.error('An error occurred in calculating user web-of-trust!')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, [ndk, userState.user, userWotLevel, dispatch])
|
||||||
|
|
||||||
|
// get site's wot level
|
||||||
|
useEffect(() => {
|
||||||
|
const SITE_WOT_NPUB = import.meta.env.VITE_SITE_WOT_NPUB
|
||||||
|
const hexPubkey = npubToHex(SITE_WOT_NPUB)
|
||||||
|
if (hexPubkey) {
|
||||||
|
fetchEventFromUserRelays(
|
||||||
|
{
|
||||||
|
kinds: [NDKKind.AppSpecificData],
|
||||||
|
'#d': ['degmods']
|
||||||
|
},
|
||||||
|
hexPubkey,
|
||||||
|
UserRelaysType.Both
|
||||||
|
).then((event) => {
|
||||||
|
if (event) {
|
||||||
|
const wot = event.tagValue('wot')
|
||||||
|
if (wot) dispatch(setSiteWotLevel(parseInt(wot)))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, [dispatch, fetchEventFromUserRelays])
|
||||||
|
|
||||||
|
// get user's wot level
|
||||||
|
useEffect(() => {
|
||||||
|
if (userState.user?.pubkey) {
|
||||||
|
const hexPubkey = npubToHex(userState.user.pubkey as string)
|
||||||
|
|
||||||
|
if (hexPubkey) {
|
||||||
|
fetchEventFromUserRelays(
|
||||||
|
{
|
||||||
|
kinds: [NDKKind.AppSpecificData],
|
||||||
|
'#d': ['degmods']
|
||||||
|
},
|
||||||
|
hexPubkey,
|
||||||
|
UserRelaysType.Both
|
||||||
|
).then((event) => {
|
||||||
|
if (event) {
|
||||||
|
const wot = event.tagValue('wot')
|
||||||
|
if (wot) dispatch(setUserWotLevel(parseInt(wot)))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [userState.user, dispatch, fetchEventFromUserRelays])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head />
|
<Head />
|
||||||
<Header />
|
<Header />
|
||||||
<Outlet />
|
{siteWotStatus === WOTStatus.LOADED && <Outlet />}
|
||||||
|
{siteWotStatus === WOTStatus.LOADING && (
|
||||||
|
<LoadingSpinner desc="Loading site's web-of-trust" />
|
||||||
|
)}
|
||||||
|
{siteWotStatus === WOTStatus.FAILED && (
|
||||||
|
<h3>Failed to load site's web-of-trust</h3>
|
||||||
|
)}
|
||||||
<Footer />
|
<Footer />
|
||||||
<SocialNav />
|
<SocialNav />
|
||||||
<ScrollRestoration />
|
<ScrollRestoration />
|
||||||
|
@ -8,6 +8,7 @@ import { GameCard } from '../components/GameCard'
|
|||||||
import { ModCard } from '../components/ModCard'
|
import { ModCard } from '../components/ModCard'
|
||||||
import { LANDING_PAGE_DATA, PROFILE_BLOG_FILTER_LIMIT } from '../constants'
|
import { LANDING_PAGE_DATA, PROFILE_BLOG_FILTER_LIMIT } from '../constants'
|
||||||
import {
|
import {
|
||||||
|
useAppSelector,
|
||||||
useDidMount,
|
useDidMount,
|
||||||
useGames,
|
useGames,
|
||||||
useLocalStorage,
|
useLocalStorage,
|
||||||
@ -247,6 +248,7 @@ const DisplayMod = ({ naddr }: DisplayModProps) => {
|
|||||||
const DisplayLatestMods = () => {
|
const DisplayLatestMods = () => {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const { fetchMods } = useNDKContext()
|
const { fetchMods } = useNDKContext()
|
||||||
|
const { siteWot, userWot } = useAppSelector((state) => state.wot)
|
||||||
const [isFetchingLatestMods, setIsFetchingLatestMods] = useState(true)
|
const [isFetchingLatestMods, setIsFetchingLatestMods] = useState(true)
|
||||||
const [latestMods, setLatestMods] = useState<ModDetails[]>([])
|
const [latestMods, setLatestMods] = useState<ModDetails[]>([])
|
||||||
|
|
||||||
@ -258,7 +260,10 @@ const DisplayLatestMods = () => {
|
|||||||
.then((mods) => {
|
.then((mods) => {
|
||||||
// Sort by the latest (published_at descending)
|
// Sort by the latest (published_at descending)
|
||||||
mods.sort((a, b) => b.published_at - a.published_at)
|
mods.sort((a, b) => b.published_at - a.published_at)
|
||||||
setLatestMods(mods)
|
const wotFilteredMods = mods.filter(
|
||||||
|
(mod) => siteWot.includes(mod.author) || userWot.includes(mod.author)
|
||||||
|
)
|
||||||
|
setLatestMods(wotFilteredMods)
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
setIsFetchingLatestMods(false)
|
setIsFetchingLatestMods(false)
|
||||||
|
@ -1,6 +1,93 @@
|
|||||||
|
import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk'
|
||||||
|
import { LoadingSpinner } from 'components/LoadingSpinner'
|
||||||
|
import { useAppDispatch, useAppSelector, useNDKContext } from 'hooks'
|
||||||
|
import { kinds, UnsignedEvent, Event } from 'nostr-tools'
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import { toast } from 'react-toastify'
|
||||||
|
import { setUserWotLevel } from 'store/reducers/wot'
|
||||||
|
import { UserRelaysType } from 'types'
|
||||||
|
import { log, LogType, now } from 'utils'
|
||||||
|
|
||||||
// todo: use components from Input.tsx
|
// todo: use components from Input.tsx
|
||||||
export const PreferencesSetting = () => {
|
export const PreferencesSetting = () => {
|
||||||
|
const dispatch = useAppDispatch()
|
||||||
|
|
||||||
|
const [wotLevel, setWotLevel] = useState(3)
|
||||||
|
const [isSaving, setIsSaving] = useState(false)
|
||||||
|
const { ndk, fetchEventFromUserRelays, publish } = useNDKContext()
|
||||||
|
const user = useAppSelector((state) => state.user.user)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (user?.pubkey) {
|
||||||
|
fetchEventFromUserRelays(
|
||||||
|
{
|
||||||
|
kinds: [NDKKind.AppSpecificData],
|
||||||
|
'#d': ['degmods']
|
||||||
|
},
|
||||||
|
user.pubkey as string,
|
||||||
|
UserRelaysType.Both
|
||||||
|
).then((event) => {
|
||||||
|
if (event) {
|
||||||
|
console.log('event :>> ', event)
|
||||||
|
const wot = event.tagValue('wot')
|
||||||
|
if (wot) setWotLevel(parseInt(wot))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, [user, fetchEventFromUserRelays])
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
setIsSaving(true)
|
||||||
|
|
||||||
|
let hexPubkey: string
|
||||||
|
|
||||||
|
if (user?.pubkey) {
|
||||||
|
hexPubkey = user.pubkey as string
|
||||||
|
} else {
|
||||||
|
hexPubkey = (await window.nostr?.getPublicKey()) as string
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hexPubkey) {
|
||||||
|
toast.error('Could not get pubkey')
|
||||||
|
setIsSaving(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const unsignedEvent: UnsignedEvent = {
|
||||||
|
kind: kinds.Application,
|
||||||
|
created_at: now(),
|
||||||
|
pubkey: hexPubkey,
|
||||||
|
content: '',
|
||||||
|
tags: [
|
||||||
|
['d', 'degmods'],
|
||||||
|
['wot', wotLevel.toString()]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
const signedEvent = await window.nostr
|
||||||
|
?.signEvent(unsignedEvent)
|
||||||
|
.then((event) => event as Event)
|
||||||
|
.catch((err) => {
|
||||||
|
toast.error('Failed to sign the event!')
|
||||||
|
log(true, LogType.Error, 'Failed to sign the event!', err)
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!signedEvent) {
|
||||||
|
setIsSaving(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const ndkEvent = new NDKEvent(ndk, signedEvent)
|
||||||
|
await publish(ndkEvent)
|
||||||
|
dispatch(setUserWotLevel(wotLevel))
|
||||||
|
|
||||||
|
setIsSaving(false)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
|
{isSaving && <LoadingSpinner desc='Saving preferences to relays' />}
|
||||||
<div className='IBMSMSplitMainFullSideFWMid'>
|
<div className='IBMSMSplitMainFullSideFWMid'>
|
||||||
<div className='IBMSMSplitMainFullSideSec'>
|
<div className='IBMSMSplitMainFullSideSec'>
|
||||||
<div className='IBMSMSMBS_Write'>
|
<div className='IBMSMSMBS_Write'>
|
||||||
@ -93,12 +180,13 @@ export const PreferencesSetting = () => {
|
|||||||
type='range'
|
type='range'
|
||||||
max='100'
|
max='100'
|
||||||
min='0'
|
min='0'
|
||||||
value='10'
|
value={wotLevel}
|
||||||
|
onChange={(e) => setWotLevel(parseInt(e.target.value))}
|
||||||
step='1'
|
step='1'
|
||||||
required
|
required
|
||||||
name='WoTLevel'
|
name='WoTLevel'
|
||||||
/>
|
/>
|
||||||
<p className='ZapSplitUserBoxRangeText'>10</p>
|
<p className='ZapSplitUserBoxRangeText'>{wotLevel}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className='inputLabelWrapperMain inputLabelWrapperMainAlt inputLabelWrapperMainAltStylized'>
|
<div className='inputLabelWrapperMain inputLabelWrapperMainAlt inputLabelWrapperMainAltStylized'>
|
||||||
<label className='form-label labelMain'>
|
<label className='form-label labelMain'>
|
||||||
@ -113,12 +201,17 @@ export const PreferencesSetting = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='IBMSMSMBS_WriteAction'>
|
<div className='IBMSMSMBS_WriteAction'>
|
||||||
<button className='btn btnMain' type='button'>
|
<button
|
||||||
|
className='btn btnMain'
|
||||||
|
type='button'
|
||||||
|
onClick={handleSave}
|
||||||
|
>
|
||||||
Save
|
Save
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
import { configureStore } from '@reduxjs/toolkit'
|
import { configureStore } from '@reduxjs/toolkit'
|
||||||
import userReducer from './reducers/user'
|
import userReducer from './reducers/user'
|
||||||
|
import wotReducer from './reducers/wot'
|
||||||
|
|
||||||
export const store = configureStore({
|
export const store = configureStore({
|
||||||
reducer: {
|
reducer: {
|
||||||
user: userReducer
|
user: userReducer,
|
||||||
|
wot: wotReducer
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
84
src/store/reducers/wot.ts
Normal file
84
src/store/reducers/wot.ts
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
|
||||||
|
|
||||||
|
export enum WOTStatus {
|
||||||
|
IDLE, // Not started
|
||||||
|
LOADING, // Currently loading
|
||||||
|
LOADED, // Successfully loaded
|
||||||
|
FAILED // Failed to load
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IWOT {
|
||||||
|
siteWot: string[]
|
||||||
|
siteWotStatus: WOTStatus
|
||||||
|
siteWotLevel: number
|
||||||
|
userWot: string[]
|
||||||
|
userWotStatus: WOTStatus
|
||||||
|
userWotLevel: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState: IWOT = {
|
||||||
|
siteWot: [],
|
||||||
|
siteWotStatus: WOTStatus.IDLE,
|
||||||
|
siteWotLevel: 3,
|
||||||
|
userWot: [],
|
||||||
|
userWotStatus: WOTStatus.IDLE,
|
||||||
|
userWotLevel: 3
|
||||||
|
}
|
||||||
|
|
||||||
|
export const wotSlice = createSlice({
|
||||||
|
name: 'wot',
|
||||||
|
initialState,
|
||||||
|
reducers: {
|
||||||
|
setSiteWot(state, action: PayloadAction<string[]>) {
|
||||||
|
state = {
|
||||||
|
...state,
|
||||||
|
siteWot: action.payload,
|
||||||
|
siteWotStatus: WOTStatus.LOADED
|
||||||
|
}
|
||||||
|
return state
|
||||||
|
},
|
||||||
|
setUserWot(state, action: PayloadAction<string[]>) {
|
||||||
|
state = {
|
||||||
|
...state,
|
||||||
|
userWot: action.payload,
|
||||||
|
userWotStatus: WOTStatus.LOADED
|
||||||
|
}
|
||||||
|
return state
|
||||||
|
},
|
||||||
|
setSiteWotStatus(state, action: PayloadAction<WOTStatus>) {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
siteWotStatus: action.payload
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setUserWotStatus(state, action: PayloadAction<WOTStatus>) {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
userWotStatus: action.payload
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setSiteWotLevel(state, action: PayloadAction<number>) {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
siteWotLevel: action.payload
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setUserWotLevel(state, action: PayloadAction<number>) {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
userWotLevel: action.payload
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export const {
|
||||||
|
setSiteWot,
|
||||||
|
setUserWot,
|
||||||
|
setSiteWotStatus,
|
||||||
|
setUserWotStatus,
|
||||||
|
setSiteWotLevel,
|
||||||
|
setUserWotLevel
|
||||||
|
} = wotSlice.actions
|
||||||
|
|
||||||
|
export default wotSlice.reducer
|
@ -17,9 +17,17 @@ export enum ModeratedFilter {
|
|||||||
Unmoderated_Fully = 'Unmoderated Fully'
|
Unmoderated_Fully = 'Unmoderated Fully'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum WOTFilterOptions {
|
||||||
|
Site_And_Mine = 'Site & Mine',
|
||||||
|
Site_Only = 'Site Only',
|
||||||
|
Mine_Only = 'Mine Only',
|
||||||
|
None = 'None'
|
||||||
|
}
|
||||||
|
|
||||||
export interface FilterOptions {
|
export interface FilterOptions {
|
||||||
sort: SortBy
|
sort: SortBy
|
||||||
nsfw: NSFWFilter
|
nsfw: NSFWFilter
|
||||||
source: string
|
source: string
|
||||||
moderated: ModeratedFilter
|
moderated: ModeratedFilter
|
||||||
|
wot: WOTFilterOptions
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,15 @@
|
|||||||
import { FilterOptions, SortBy, NSFWFilter, ModeratedFilter } from 'types'
|
import {
|
||||||
|
FilterOptions,
|
||||||
|
SortBy,
|
||||||
|
NSFWFilter,
|
||||||
|
ModeratedFilter,
|
||||||
|
WOTFilterOptions
|
||||||
|
} from 'types'
|
||||||
|
|
||||||
export const DEFAULT_FILTER_OPTIONS: FilterOptions = {
|
export const DEFAULT_FILTER_OPTIONS: FilterOptions = {
|
||||||
sort: SortBy.Latest,
|
sort: SortBy.Latest,
|
||||||
nsfw: NSFWFilter.Hide_NSFW,
|
nsfw: NSFWFilter.Hide_NSFW,
|
||||||
source: window.location.host,
|
source: window.location.host,
|
||||||
moderated: ModeratedFilter.Moderated
|
moderated: ModeratedFilter.Moderated,
|
||||||
|
wot: WOTFilterOptions.Site_Only
|
||||||
}
|
}
|
||||||
|
147
src/utils/wot.ts
Normal file
147
src/utils/wot.ts
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
import NDK, {
|
||||||
|
Hexpubkey,
|
||||||
|
NDKFilter,
|
||||||
|
NDKKind,
|
||||||
|
NDKSubscriptionCacheUsage,
|
||||||
|
NDKTag
|
||||||
|
} from '@nostr-dev-kit/ndk'
|
||||||
|
import { nip19 } from 'nostr-tools'
|
||||||
|
|
||||||
|
interface UserRelations {
|
||||||
|
follows: Set<Hexpubkey>
|
||||||
|
muted: Set<Hexpubkey>
|
||||||
|
}
|
||||||
|
|
||||||
|
type Network = Map<Hexpubkey, UserRelations>
|
||||||
|
|
||||||
|
export const calculateWot = async (
|
||||||
|
pubkey: Hexpubkey,
|
||||||
|
ndk: NDK,
|
||||||
|
targetWOTScore: number
|
||||||
|
) => {
|
||||||
|
const network: Network = new Map()
|
||||||
|
const WOT = new Set<Hexpubkey>()
|
||||||
|
|
||||||
|
const userRelations = await findFollowsAndMuteUsers(pubkey, ndk)
|
||||||
|
network.set(pubkey, userRelations)
|
||||||
|
|
||||||
|
// find the userRelations of every user in follow list
|
||||||
|
const follows = Array.from(userRelations.follows)
|
||||||
|
const promises = follows.map((user) =>
|
||||||
|
findFollowsAndMuteUsers(user, ndk).then((userRelations) => {
|
||||||
|
network.set(user, userRelations)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
await Promise.all(promises)
|
||||||
|
|
||||||
|
// add all the following users to WOT
|
||||||
|
follows.forEach((f) => {
|
||||||
|
WOT.add(f)
|
||||||
|
})
|
||||||
|
|
||||||
|
// construct a list of users not being followed directly
|
||||||
|
const indirectFollows = new Set<Hexpubkey>()
|
||||||
|
follows.forEach((hexpubkey) => {
|
||||||
|
const relations = network.get(hexpubkey)
|
||||||
|
if (relations) {
|
||||||
|
relations.follows.forEach((f) => {
|
||||||
|
indirectFollows.add(f)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
indirectFollows.forEach((targetHexPubkey) => {
|
||||||
|
// if any of the indirect followed user is in direct mute list
|
||||||
|
// we'll not include it in WOT
|
||||||
|
if (userRelations.muted.has(targetHexPubkey)) return
|
||||||
|
|
||||||
|
const wotScore = calculateWoTScore(pubkey, targetHexPubkey, network)
|
||||||
|
if (wotScore >= targetWOTScore) {
|
||||||
|
WOT.add(targetHexPubkey)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return WOT
|
||||||
|
}
|
||||||
|
|
||||||
|
export const calculateWoTScore = (
|
||||||
|
user: Hexpubkey,
|
||||||
|
targetUser: Hexpubkey,
|
||||||
|
network: Network
|
||||||
|
): number => {
|
||||||
|
const userRelations = network.get(user)
|
||||||
|
if (!userRelations) return 0
|
||||||
|
|
||||||
|
let wotScore = 0
|
||||||
|
|
||||||
|
// Check each user followed
|
||||||
|
for (const followedUser of userRelations.follows) {
|
||||||
|
const followedUserRelations = network.get(followedUser)
|
||||||
|
if (!followedUserRelations) continue
|
||||||
|
|
||||||
|
// Positive Score: +1 if followedUser also follows targetId
|
||||||
|
if (followedUserRelations.follows.has(targetUser)) {
|
||||||
|
wotScore += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Negative Score: -1 if followedUser has muted targetId
|
||||||
|
if (followedUserRelations.muted.has(targetUser)) {
|
||||||
|
wotScore -= 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return wotScore
|
||||||
|
}
|
||||||
|
|
||||||
|
export const findFollowsAndMuteUsers = async (
|
||||||
|
pubkey: string,
|
||||||
|
ndk: NDK
|
||||||
|
): Promise<UserRelations> => {
|
||||||
|
const follows = new Set<Hexpubkey>()
|
||||||
|
const muted = new Set<Hexpubkey>()
|
||||||
|
|
||||||
|
const filter: NDKFilter = {
|
||||||
|
kinds: [NDKKind.Contacts, NDKKind.MuteList],
|
||||||
|
authors: [pubkey]
|
||||||
|
}
|
||||||
|
|
||||||
|
const events = await ndk.fetchEvents(filter, {
|
||||||
|
groupable: false,
|
||||||
|
closeOnEose: true,
|
||||||
|
cacheUsage: NDKSubscriptionCacheUsage.PARALLEL
|
||||||
|
})
|
||||||
|
|
||||||
|
events.forEach((event) => {
|
||||||
|
if (event.kind === NDKKind.Contacts) {
|
||||||
|
filterValidPTags(event.tags).forEach((f) => {
|
||||||
|
follows.add(f)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
events.forEach((event) => {
|
||||||
|
if (event.kind === NDKKind.MuteList) {
|
||||||
|
filterValidPTags(event.tags).forEach((f) => {
|
||||||
|
muted.add(f)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
follows,
|
||||||
|
muted
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const filterValidPTags = (tags: NDKTag[]) =>
|
||||||
|
tags
|
||||||
|
.filter((t: NDKTag) => t[0] === 'p')
|
||||||
|
.map((t: NDKTag) => t[1])
|
||||||
|
.filter((f: Hexpubkey) => {
|
||||||
|
try {
|
||||||
|
nip19.npubEncode(f)
|
||||||
|
return true
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
})
|
1
src/vite-env.d.ts
vendored
1
src/vite-env.d.ts
vendored
@ -4,6 +4,7 @@ interface ImportMetaEnv {
|
|||||||
readonly VITE_APP_RELAY: string
|
readonly VITE_APP_RELAY: string
|
||||||
readonly VITE_ADMIN_NPUBS: string
|
readonly VITE_ADMIN_NPUBS: string
|
||||||
readonly VITE_REPORTING_NPUB: string
|
readonly VITE_REPORTING_NPUB: string
|
||||||
|
readonly VITE_SITE_WOT_NPUB: string
|
||||||
readonly VITE_FALLBACK_MOD_IMAGE: string
|
readonly VITE_FALLBACK_MOD_IMAGE: string
|
||||||
readonly VITE_FALLBACK_GAME_IMAGE: string
|
readonly VITE_FALLBACK_GAME_IMAGE: string
|
||||||
readonly VITE_BLOG_NPUBS: string
|
readonly VITE_BLOG_NPUBS: string
|
||||||
|
Loading…
Reference in New Issue
Block a user