fix: improve wot logic #155

Merged
s merged 2 commits from wot-fixes into staging 2024-11-18 18:25:52 +00:00
6 changed files with 116 additions and 110 deletions
Showing only changes of commit 4f8cac6eee - Show all commits

View File

@ -11,6 +11,7 @@ import {
} from 'types' } from 'types'
import { npubToHex } from 'utils' import { npubToHex } from 'utils'
import { useAppSelector } from './redux' import { useAppSelector } from './redux'
import { isInWoT } from 'utils/wot'
export const useFilteredMods = ( export const useFilteredMods = (
mods: ModDetails[], mods: ModDetails[],
@ -23,7 +24,9 @@ export const useFilteredMods = (
}, },
author?: string | undefined author?: string | undefined
) => { ) => {
const { siteWot, userWot } = useAppSelector((state) => state.wot) const { siteWot, siteWotLevel, userWot, userWotLevel } = useAppSelector(
(state) => state.wot
)
return useMemo(() => { return useMemo(() => {
const nsfwFilter = (mods: ModDetails[]) => { const nsfwFilter = (mods: ModDetails[]) => {
@ -56,13 +59,18 @@ export const useFilteredMods = (
case WOTFilterOptions.None: case WOTFilterOptions.None:
return mods return mods
case WOTFilterOptions.Site_Only: case WOTFilterOptions.Site_Only:
return mods.filter((mod) => siteWot.includes(mod.author)) return mods.filter((mod) =>
isInWoT(siteWot, siteWotLevel, mod.author)
)
case WOTFilterOptions.Mine_Only: case WOTFilterOptions.Mine_Only:
return mods.filter((mod) => userWot.includes(mod.author)) return mods.filter((mod) =>
isInWoT(userWot, userWotLevel, mod.author)
)
case WOTFilterOptions.Site_And_Mine: case WOTFilterOptions.Site_And_Mine:
return mods.filter( return mods.filter(
(mod) => (mod) =>
siteWot.includes(mod.author) || userWot.includes(mod.author) isInWoT(siteWot, siteWotLevel, mod.author) ||
isInWoT(userWot, userWotLevel, mod.author)
) )
} }
} }
@ -114,6 +122,8 @@ export const useFilteredMods = (
muteLists, muteLists,
nsfwList, nsfwList,
siteWot, siteWot,
userWot siteWotLevel,
userWot,
userWotLevel
]) ])
} }

View File

@ -21,7 +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' import { resetUserWot } from 'store/reducers/wot'
export const Header = () => { export const Header = () => {
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
@ -49,7 +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([])) dispatch(resetUserWot())
} else { } else {
dispatch( dispatch(
setAuth({ setAuth({
@ -381,13 +381,29 @@ const RegisterButtonWithDialog = () => {
Browser Extensions (Windows) Browser Extensions (Windows)
</label> </label>
<p className='labelDescriptionMain'> <p className='labelDescriptionMain'>
Once you create your "account" on any of these, come back and click login, then sign-in with Once you create your "account" on any of these, come
extension. Here's a quick video guide, and here's a <a back and click login, then sign-in with extension.
href='https://degmods.com/blog/naddr1qvzqqqr4gupzpa9lr76m4zlg88mscue3wvlrp8mcpq3txy0k8cqlnhy2hw6z37x4qqjrzcfc8qurjefn943xyen9956rywp595unjc3h94nxvwfexymxxcfnvdjxxlyq37c' Here's a quick video guide, and here's a{' '}
>guide post</a> to help with this process.</p> <a href='https://degmods.com/blog/naddr1qvzqqqr4gupzpa9lr76m4zlg88mscue3wvlrp8mcpq3txy0k8cqlnhy2hw6z37x4qqjrzcfc8qurjefn943xyen9956rywp595unjc3h94nxvwfexymxxcfnvdjxxlyq37c'>
<div style={{ width: '100%', height: 'auto', borderRadius: '8px', overflow: 'hidden' }}> guide post
<video controls style={{ width: '100%' }}><source src="https://video.nostr.build/765aa9bf16dd58bca701efee2572f7e77f29b2787cddd2bee8bbbdea35798153.mp4" type="video/mp4" /> </a>{' '}
Your browser does not support the video tag.</video> to help with this process.
</p>
<div
style={{
width: '100%',
height: 'auto',
borderRadius: '8px',
overflow: 'hidden'
}}
>
<video controls style={{ width: '100%' }}>
<source
src='https://video.nostr.build/765aa9bf16dd58bca701efee2572f7e77f29b2787cddd2bee8bbbdea35798153.mp4'
type='video/mp4'
/>
Your browser does not support the video tag.
</video>
</div> </div>
</div> </div>
<a <a

View File

@ -35,10 +35,9 @@ export const Layout = () => {
const hexPubkey = npubToHex(SITE_WOT_NPUB) const hexPubkey = npubToHex(SITE_WOT_NPUB)
if (hexPubkey) { if (hexPubkey) {
dispatch(setSiteWotStatus(WOTStatus.LOADING)) dispatch(setSiteWotStatus(WOTStatus.LOADING))
calculateWot(hexPubkey, ndk, siteWotLevel) calculateWot(hexPubkey, ndk)
.then((wot) => { .then((wot) => {
dispatch(setSiteWot(Array.from(wot))) dispatch(setSiteWot(wot))
dispatch(setSiteWotStatus(WOTStatus.LOADED))
}) })
.catch((err) => { .catch((err) => {
console.trace('An error occurred in calculating site WOT', err) console.trace('An error occurred in calculating site WOT', err)
@ -54,9 +53,9 @@ export const Layout = () => {
if (ndk && userState.user?.pubkey) { if (ndk && userState.user?.pubkey) {
const hexPubkey = npubToHex(userState.user.pubkey as string) const hexPubkey = npubToHex(userState.user.pubkey as string)
if (hexPubkey) if (hexPubkey)
calculateWot(hexPubkey, ndk, userWotLevel) calculateWot(hexPubkey, ndk)
.then((wot) => { .then((wot) => {
dispatch(setUserWot(Array.from(wot))) dispatch(setUserWot(wot))
}) })
.catch((err) => { .catch((err) => {
console.trace('An error occurred in calculating user WOT', err) console.trace('An error occurred in calculating user WOT', err)

View File

@ -38,6 +38,7 @@ import 'swiper/css/navigation'
import 'swiper/css/pagination' import 'swiper/css/pagination'
import { LoadingSpinner } from 'components/LoadingSpinner' import { LoadingSpinner } from 'components/LoadingSpinner'
import { Spinner } from 'components/Spinner' import { Spinner } from 'components/Spinner'
import { isInWoT } from 'utils/wot'
export const HomePage = () => { export const HomePage = () => {
const navigate = useNavigate() const navigate = useNavigate()
@ -248,7 +249,9 @@ 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 { siteWot, siteWotLevel, userWot, userWotLevel } = useAppSelector(
(state) => state.wot
)
const [isFetchingLatestMods, setIsFetchingLatestMods] = useState(true) const [isFetchingLatestMods, setIsFetchingLatestMods] = useState(true)
const [latestMods, setLatestMods] = useState<ModDetails[]>([]) const [latestMods, setLatestMods] = useState<ModDetails[]>([])
@ -261,7 +264,9 @@ const DisplayLatestMods = () => {
// 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)
const wotFilteredMods = mods.filter( const wotFilteredMods = mods.filter(
(mod) => siteWot.includes(mod.author) || userWot.includes(mod.author) (mod) =>
isInWoT(siteWot, siteWotLevel, mod.author) ||
isInWoT(userWot, userWotLevel, mod.author)
) )
setLatestMods(wotFilteredMods) setLatestMods(wotFilteredMods)
}) })

View File

@ -8,19 +8,19 @@ export enum WOTStatus {
} }
export interface IWOT { export interface IWOT {
siteWot: string[] siteWot: Record<string, number>
siteWotStatus: WOTStatus siteWotStatus: WOTStatus
siteWotLevel: number siteWotLevel: number
userWot: string[] userWot: Record<string, number>
userWotStatus: WOTStatus userWotStatus: WOTStatus
userWotLevel: number userWotLevel: number
} }
const initialState: IWOT = { const initialState: IWOT = {
siteWot: [], siteWot: {},
siteWotStatus: WOTStatus.IDLE, siteWotStatus: WOTStatus.IDLE,
siteWotLevel: 0, siteWotLevel: 0,
userWot: [], userWot: {},
userWotStatus: WOTStatus.IDLE, userWotStatus: WOTStatus.IDLE,
userWotLevel: 0 userWotLevel: 0
} }
@ -29,45 +29,30 @@ export const wotSlice = createSlice({
name: 'wot', name: 'wot',
initialState, initialState,
reducers: { reducers: {
setSiteWot(state, action: PayloadAction<string[]>) { setSiteWot(state, action: PayloadAction<Record<string, number>>) {
state = { state.siteWot = action.payload
...state, state.siteWotStatus = WOTStatus.LOADED
siteWot: action.payload,
siteWotStatus: WOTStatus.LOADED
}
return state
}, },
setUserWot(state, action: PayloadAction<string[]>) { setUserWot(state, action: PayloadAction<Record<string, number>>) {
state = { state.userWot = action.payload
...state, state.userWotStatus = WOTStatus.LOADED
userWot: action.payload, },
userWotStatus: WOTStatus.LOADED resetUserWot(state) {
} state.userWot = {}
return state state.userWotStatus = WOTStatus.IDLE
state.userWotLevel = 0
}, },
setSiteWotStatus(state, action: PayloadAction<WOTStatus>) { setSiteWotStatus(state, action: PayloadAction<WOTStatus>) {
return { state.siteWotStatus = action.payload
...state,
siteWotStatus: action.payload
}
}, },
setUserWotStatus(state, action: PayloadAction<WOTStatus>) { setUserWotStatus(state, action: PayloadAction<WOTStatus>) {
return { state.userWotStatus = action.payload
...state,
userWotStatus: action.payload
}
}, },
setSiteWotLevel(state, action: PayloadAction<number>) { setSiteWotLevel(state, action: PayloadAction<number>) {
return { state.siteWotLevel = action.payload
...state,
siteWotLevel: action.payload
}
}, },
setUserWotLevel(state, action: PayloadAction<number>) { setUserWotLevel(state, action: PayloadAction<number>) {
return { state.userWotLevel = action.payload
...state,
userWotLevel: action.payload
}
} }
} }
}) })
@ -78,7 +63,8 @@ export const {
setSiteWotStatus, setSiteWotStatus,
setUserWotStatus, setUserWotStatus,
setSiteWotLevel, setSiteWotLevel,
setUserWotLevel setUserWotLevel,
resetUserWot
} = wotSlice.actions } = wotSlice.actions
export default wotSlice.reducer export default wotSlice.reducer

View File

@ -14,19 +14,26 @@ interface UserRelations {
type Network = Map<Hexpubkey, UserRelations> type Network = Map<Hexpubkey, UserRelations>
export const calculateWot = async ( export const calculateWot = async (pubkey: Hexpubkey, ndk: NDK) => {
pubkey: Hexpubkey, const WoT: Record<string, number> = {}
ndk: NDK,
targetWOTScore: number
) => {
const network: Network = new Map()
const WOT = new Set<Hexpubkey>()
const userRelations = await findFollowsAndMuteUsers(pubkey, ndk) const userRelations = await findFollowsAndMuteUsers(pubkey, ndk)
network.set(pubkey, userRelations) const follows = Array.from(userRelations.follows)
const muted = Array.from(userRelations.muted)
// Add all the following users to WoT with score set to Positive_Infinity
follows.forEach((f) => {
WoT[f] = Number.POSITIVE_INFINITY
})
// Add all the muted users to WoT with score set to Negative_Infinity
muted.forEach((m) => {
WoT[m] = Number.NEGATIVE_INFINITY
})
const network: Network = new Map()
// find the userRelations of every user in follow list // find the userRelations of every user in follow list
const follows = Array.from(userRelations.follows)
const promises = follows.map((user) => const promises = follows.map((user) =>
findFollowsAndMuteUsers(user, ndk).then((userRelations) => { findFollowsAndMuteUsers(user, ndk).then((userRelations) => {
network.set(user, userRelations) network.set(user, userRelations)
@ -34,61 +41,35 @@ export const calculateWot = async (
) )
await Promise.all(promises) await Promise.all(promises)
// add all the following users to WOT // make a list of all the users in the network either mutes or followed
follows.forEach((f) => { const users = new Set<Hexpubkey>()
WOT.add(f) const userRelationsArray = Array.from(network.values())
userRelationsArray.forEach(({ follows, muted }) => {
follows.forEach((f) => users.add(f))
muted.forEach((m) => users.add(m))
}) })
// construct a list of users not being followed directly users.forEach((user) => {
const indirectFollows = new Set<Hexpubkey>() // Only calculate if it's not already added to WoT
follows.forEach((hexpubkey) => { if (!(user in WoT)) {
const relations = network.get(hexpubkey) const wotScore = calculateWoTScore(user, network)
if (relations) { WoT[user] = wotScore
relations.follows.forEach((f) => {
indirectFollows.add(f)
})
} }
}) })
indirectFollows.forEach((targetHexPubkey) => { return WoT
// 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 = ( export const calculateWoTScore = (user: Hexpubkey, network: Network) => {
user: Hexpubkey,
targetUser: Hexpubkey,
network: Network
): number => {
const userRelations = network.get(user)
if (!userRelations) return 0
let wotScore = 0 let wotScore = 0
// Check each user followed // iterate over all the entries in the network and increment/decrement
for (const followedUser of userRelations.follows) { // wotScore based on the list user in which exists (followed/mutes)
const followedUserRelations = network.get(followedUser) network.forEach(({ follows, muted }) => {
if (!followedUserRelations) continue if (follows.has(user)) wotScore += 1
// Positive Score: +1 if followedUser also follows targetId if (muted.has(user)) wotScore -= 1
if (followedUserRelations.follows.has(targetUser)) { })
wotScore += 1
}
// Negative Score: -1 if followedUser has muted targetId
if (followedUserRelations.muted.has(targetUser)) {
wotScore -= 1
}
}
return wotScore return wotScore
} }
@ -145,3 +126,12 @@ export const filterValidPTags = (tags: NDKTag[]) =>
return false return false
} }
}) })
export const isInWoT = (
WoT: Record<string, number>,
targetScore: number,
targetUser: string
): boolean => {
const wotScore = WoT[targetUser] ?? 0 // Default to 0 if the user is not in the record
return wotScore >= targetScore
}