Merge pull request 'fix: improve wot logic' (#155) from wot-fixes into staging
All checks were successful
Release to Staging / build_and_release (push) Successful in 1m4s

Reviewed-on: #155
This commit is contained in:
s 2024-11-18 18:25:52 +00:00
commit 0b2de940d0
6 changed files with 116 additions and 110 deletions

View File

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

View File

@ -21,7 +21,7 @@ import '../styles/popup.css'
import { npubToHex } from '../utils'
import logo from '../assets/img/DEG Mods Logo With Text.svg'
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 = () => {
const dispatch = useAppDispatch()
@ -49,7 +49,7 @@ export const Header = () => {
if (opts.type === 'logout') {
dispatch(setAuth(null))
dispatch(setUser(null))
dispatch(setUserWot([]))
dispatch(resetUserWot())
} else {
dispatch(
setAuth({
@ -381,13 +381,29 @@ const RegisterButtonWithDialog = () => {
Browser Extensions (Windows)
</label>
<p className='labelDescriptionMain'>
Once you create your "account" on any of these, come back and click login, then sign-in with
extension. Here's a quick video guide, and here's a <a
href='https://degmods.com/blog/naddr1qvzqqqr4gupzpa9lr76m4zlg88mscue3wvlrp8mcpq3txy0k8cqlnhy2hw6z37x4qqjrzcfc8qurjefn943xyen9956rywp595unjc3h94nxvwfexymxxcfnvdjxxlyq37c'
>guide post</a> 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>
Once you create your "account" on any of these, come
back and click login, then sign-in with extension.
Here's a quick video guide, and here's a{' '}
<a href='https://degmods.com/blog/naddr1qvzqqqr4gupzpa9lr76m4zlg88mscue3wvlrp8mcpq3txy0k8cqlnhy2hw6z37x4qqjrzcfc8qurjefn943xyen9956rywp595unjc3h94nxvwfexymxxcfnvdjxxlyq37c'>
guide post
</a>{' '}
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>
<a

View File

@ -35,10 +35,9 @@ export const Layout = () => {
const hexPubkey = npubToHex(SITE_WOT_NPUB)
if (hexPubkey) {
dispatch(setSiteWotStatus(WOTStatus.LOADING))
calculateWot(hexPubkey, ndk, siteWotLevel)
calculateWot(hexPubkey, ndk)
.then((wot) => {
dispatch(setSiteWot(Array.from(wot)))
dispatch(setSiteWotStatus(WOTStatus.LOADED))
dispatch(setSiteWot(wot))
})
.catch((err) => {
console.trace('An error occurred in calculating site WOT', err)
@ -54,9 +53,9 @@ export const Layout = () => {
if (ndk && userState.user?.pubkey) {
const hexPubkey = npubToHex(userState.user.pubkey as string)
if (hexPubkey)
calculateWot(hexPubkey, ndk, userWotLevel)
calculateWot(hexPubkey, ndk)
.then((wot) => {
dispatch(setUserWot(Array.from(wot)))
dispatch(setUserWot(wot))
})
.catch((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 { LoadingSpinner } from 'components/LoadingSpinner'
import { Spinner } from 'components/Spinner'
import { isInWoT } from 'utils/wot'
export const HomePage = () => {
const navigate = useNavigate()
@ -248,7 +249,9 @@ const DisplayMod = ({ naddr }: DisplayModProps) => {
const DisplayLatestMods = () => {
const navigate = useNavigate()
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 [latestMods, setLatestMods] = useState<ModDetails[]>([])
@ -261,7 +264,9 @@ const DisplayLatestMods = () => {
// Sort by the latest (published_at descending)
mods.sort((a, b) => b.published_at - a.published_at)
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)
})

View File

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

View File

@ -14,19 +14,26 @@ interface UserRelations {
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>()
export const calculateWot = async (pubkey: Hexpubkey, ndk: NDK) => {
const WoT: Record<string, number> = {}
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
const follows = Array.from(userRelations.follows)
const promises = follows.map((user) =>
findFollowsAndMuteUsers(user, ndk).then((userRelations) => {
network.set(user, userRelations)
@ -34,61 +41,35 @@ export const calculateWot = async (
)
await Promise.all(promises)
// add all the following users to WOT
follows.forEach((f) => {
WOT.add(f)
// make a list of all the users in the network either mutes or followed
const users = new Set<Hexpubkey>()
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
const indirectFollows = new Set<Hexpubkey>()
follows.forEach((hexpubkey) => {
const relations = network.get(hexpubkey)
if (relations) {
relations.follows.forEach((f) => {
indirectFollows.add(f)
})
users.forEach((user) => {
// Only calculate if it's not already added to WoT
if (!(user in WoT)) {
const wotScore = calculateWoTScore(user, network)
WoT[user] = wotScore
}
})
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
return WoT
}
export const calculateWoTScore = (
user: Hexpubkey,
targetUser: Hexpubkey,
network: Network
): number => {
const userRelations = network.get(user)
if (!userRelations) return 0
export const calculateWoTScore = (user: Hexpubkey, network: Network) => {
let wotScore = 0
// Check each user followed
for (const followedUser of userRelations.follows) {
const followedUserRelations = network.get(followedUser)
if (!followedUserRelations) continue
// iterate over all the entries in the network and increment/decrement
// wotScore based on the list user in which exists (followed/mutes)
network.forEach(({ follows, muted }) => {
if (follows.has(user)) wotScore += 1
// 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
}
}
if (muted.has(user)) wotScore -= 1
})
return wotScore
}
@ -145,3 +126,12 @@ export const filterValidPTags = (tags: NDKTag[]) =>
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
}