fix: improve wot logic
Improve data structure for storing WoT Also improve WoT calculation logic
This commit is contained in:
parent
6e4e580402
commit
4f8cac6eee
@ -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
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
||||||
})
|
})
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user