Merge pull request 'WoT implemented, plus other fixes' (#160) from staging into master
All checks were successful
Release to Production / build_and_release (push) Successful in 54s

Reviewed-on: #160
This commit is contained in:
freakoverse 2024-11-20 16:39:08 +00:00
commit 4a4ccaaf37
26 changed files with 842 additions and 152 deletions

View File

@ -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

View File

@ -25,10 +25,10 @@ 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
echo "VITE_SITE_WOT_NPUB=${{ vars.VITE_SITE_WOT_NPUB }}" >> .env
cat .env cat .env
- name: Create Build - name: Create Build

View File

@ -25,10 +25,10 @@ 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
echo "VITE_SITE_WOT_NPUB=${{ vars.VITE_SITE_WOT_NPUB }}" >> .env
cat .env cat .env
- name: Create Build - name: Create Build

View File

@ -32,11 +32,11 @@ 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
echo "VITE_BLOG_NPUBS=${{ vars.VITE_BLOG_NPUBS }}" >> .env echo "VITE_BLOG_NPUBS=${{ vars.VITE_BLOG_NPUBS }}" >> .env
echo "VITE_SITE_WOT_NPUB=${{ vars.VITE_SITE_WOT_NPUB }}" >> .env
cat .env cat .env
- name: Build - name: Build
run: npm run build run: npm run build

View File

@ -3,3 +3,5 @@ Game Name,16 by 9 image,Boxart image
Minecraft,,https://image.nostr.build/b75b2d3a7855370230f2976567e2d5f913a567c57ac61adfb60c7e1102f05117.jpg Minecraft,,https://image.nostr.build/b75b2d3a7855370230f2976567e2d5f913a567c57ac61adfb60c7e1102f05117.jpg
Vintage Story,,https://image.nostr.build/9efe683d339cc864032a99047ce26b2b5c19fab1ec4dcc6d4db96e2785c44eda.png Vintage Story,,https://image.nostr.build/9efe683d339cc864032a99047ce26b2b5c19fab1ec4dcc6d4db96e2785c44eda.png
Yandere Simulator,,https://image.nostr.build/54ba56b752bb9d411cbdc1d249fa0cb74c6062a305bcd0a70ecacb61b8d50030.png Yandere Simulator,,https://image.nostr.build/54ba56b752bb9d411cbdc1d249fa0cb74c6062a305bcd0a70ecacb61b8d50030.png
Genshin Impact,,https://image.nostr.build/999fccf93cf16a2e0dd8e6f00595b0ab3b5cc6beff9fe4a52f64f427cce9aedd.jpg
Zenless Zone Zero,,https://image.nostr.build/4a9b9c2cbef619552d0c123f8794286f35710dc7ca1ca0010380a630883eb2ca.jpg
1 Game Name 16 by 9 image Boxart image
3 Minecraft https://image.nostr.build/b75b2d3a7855370230f2976567e2d5f913a567c57ac61adfb60c7e1102f05117.jpg
4 Vintage Story https://image.nostr.build/9efe683d339cc864032a99047ce26b2b5c19fab1ec4dcc6d4db96e2785c44eda.png
5 Yandere Simulator https://image.nostr.build/54ba56b752bb9d411cbdc1d249fa0cb74c6062a305bcd0a70ecacb61b8d50030.png
6 Genshin Impact https://image.nostr.build/999fccf93cf16a2e0dd8e6f00595b0ab3b5cc6beff9fe4a52f64f427cce9aedd.jpg
7 Zenless Zone Zero https://image.nostr.build/4a9b9c2cbef619552d0c123f8794286f35710dc7ca1ca0010380a630883eb2ca.jpg

View File

@ -24966,7 +24966,7 @@ Mystery of Island,,
SEARCH ALL - POTIONS,, SEARCH ALL - POTIONS,,
Sanctuary Saga Playtest,, Sanctuary Saga Playtest,,
Fantasy Grounds - Beastheart and Monstrous Companions,, Fantasy Grounds - Beastheart and Monstrous Companions,,
Dragon Age: The Veilguard,, Dragon Age: The Veilguard,,https://image.nostr.build/0ec48cc2ef08b3f09647c8403e29c5fd814dc3e4aeb2bb76add93928f3a867b0.jpg
Transport INC - Map Pack,, Transport INC - Map Pack,,
Fantasy Grounds - Dungeon Crawl Classics #97: The Queen of Elfland's Son,, Fantasy Grounds - Dungeon Crawl Classics #97: The Queen of Elfland's Son,,
cyberpunkdreams: outside edges,, cyberpunkdreams: outside edges,,

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

View File

@ -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,66 @@ 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'
>
Trust: {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 &&
!userState.auth
) {
return null
}
// when logged in user not admin
if (
item === WOTFilterOptions.None ||
item === WOTFilterOptions.Mine_Only ||
item === WOTFilterOptions.Exclude
) {
const isWoTNpub =
userState.user?.npub ===
import.meta.env.VITE_SITE_WOT_NPUB
const isOwnProfile =
author &&
userState.auth &&
userState.user?.pubkey === author
if (!(isWoTNpub || 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 +188,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

View File

@ -25,7 +25,7 @@ export const LANDING_PAGE_DATA = {
'naddr1qvzqqqr4gupzpa9lr76m4zlg88mscue3wvlrp8mcpq3txy0k8cqlnhy2hw6z37x4qqjrzcfc8qurjefn943xyen9956rywp595unjc3h94nxvwfexymxxcfnvdjxxlyq37c', 'naddr1qvzqqqr4gupzpa9lr76m4zlg88mscue3wvlrp8mcpq3txy0k8cqlnhy2hw6z37x4qqjrzcfc8qurjefn943xyen9956rywp595unjc3h94nxvwfexymxxcfnvdjxxlyq37c',
'naddr1qvzqqqr4gupzpa9lr76m4zlg88mscue3wvlrp8mcpq3txy0k8cqlnhy2hw6z37x4qqjryv3k8qenydpj94nrscmp956xgwtp94snydtz95ekgvphvfnxvvrzvyexzsvsz9y', 'naddr1qvzqqqr4gupzpa9lr76m4zlg88mscue3wvlrp8mcpq3txy0k8cqlnhy2hw6z37x4qqjryv3k8qenydpj94nrscmp956xgwtp94snydtz95ekgvphvfnxvvrzvyexzsvsz9y',
'naddr1qvzqqqr4gupzpa9lr76m4zlg88mscue3wvlrp8mcpq3txy0k8cqlnhy2hw6z37x4qq2kwjtwvahns3n0tf8j6kjxggkkz4mff499ge7xzsz', 'naddr1qvzqqqr4gupzpa9lr76m4zlg88mscue3wvlrp8mcpq3txy0k8cqlnhy2hw6z37x4qq2kwjtwvahns3n0tf8j6kjxggkkz4mff499ge7xzsz',
'naddr1qvzqqqr4gupzpa9lr76m4zlg88mscue3wvlrp8mcpq3txy0k8cqlnhy2hw6z37x4qq2573jhg9trsu6vgav9gnn4dffkzk2ww3yrjejnc2s' 'naddr1qvzqqqr4gupzpa9lr76m4zlg88mscue3wvlrp8mcpq3txy0k8cqlnhy2hw6z37x4qqjrycf5vyunyd34943kydn9956rycmp943xydpc95cxge3cvguxgcmyxsmkyzpyj60'
] ]
} }
// we use this object to check if a user has reacted positively or negatively to a post // we use this object to check if a user has reacted positively or negatively to a post

View File

@ -74,7 +74,6 @@ export const NDKContextProvider = ({ children }: { children: ReactNode }) => {
useEffect(() => { useEffect(() => {
window.onunhandledrejection = async (event: PromiseRejectionEvent) => { window.onunhandledrejection = async (event: PromiseRejectionEvent) => {
event.preventDefault() event.preventDefault()
console.log(event.reason)
if (event.reason?.name === Dexie.errnames.DatabaseClosed) { if (event.reason?.name === Dexie.errnames.DatabaseClosed) {
console.log( console.log(
'Could not open Dexie DB, probably version change. Deleting old DB and reloading...' 'Could not open Dexie DB, probably version change. Deleting old DB and reloading...'
@ -245,7 +244,7 @@ export const NDKContextProvider = ({ children }: { children: ReactNode }) => {
// Find the user's relays (10s timeout). // Find the user's relays (10s timeout).
const relayUrls = await Promise.race([ const relayUrls = await Promise.race([
getRelayListForUser(hexKey, ndk), getRelayListForUser(hexKey, ndk),
timeout(10000) timeout(3000)
]) ])
.then((ndkRelayList) => { .then((ndkRelayList) => {
if (ndkRelayList) return ndkRelayList[userRelaysType] if (ndkRelayList) return ndkRelayList[userRelaysType]
@ -265,7 +264,9 @@ export const NDKContextProvider = ({ children }: { children: ReactNode }) => {
.fetchEvents( .fetchEvents(
filter, filter,
{ closeOnEose: true, cacheUsage: NDKSubscriptionCacheUsage.PARALLEL }, { closeOnEose: true, cacheUsage: NDKSubscriptionCacheUsage.PARALLEL },
NDKRelaySet.fromRelayUrls(relayUrls, ndk, true) relayUrls.length
? NDKRelaySet.fromRelayUrls(relayUrls, ndk, true)
: undefined
) )
.then((ndkEventSet) => { .then((ndkEventSet) => {
const ndkEvents = Array.from(ndkEventSet) const ndkEvents = Array.from(ndkEventSet)
@ -369,7 +370,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)

View File

@ -66,7 +66,9 @@ export const useComments = (
closeOnEose: false, closeOnEose: false,
cacheUsage: NDKSubscriptionCacheUsage.CACHE_FIRST cacheUsage: NDKSubscriptionCacheUsage.CACHE_FIRST
}, },
NDKRelaySet.fromRelayUrls(Array.from(relayUrls), ndk) relayUrls.size
? NDKRelaySet.fromRelayUrls(Array.from(relayUrls), ndk)
: undefined
) )
subscription.on('event', (ndkEvent) => { subscription.on('event', (ndkEvent) => {

View File

@ -6,9 +6,12 @@ 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'
import { isInWoT } from 'utils/wot'
export const useFilteredMods = ( export const useFilteredMods = (
mods: ModDetails[], mods: ModDetails[],
@ -21,6 +24,10 @@ export const useFilteredMods = (
}, },
author?: string | undefined author?: string | undefined
) => { ) => {
const { siteWot, siteWotLevel, userWot, userWotLevel } = 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 +53,49 @@ export const useFilteredMods = (
} }
} }
const wotFilter = (mods: ModDetails[]) => {
// Determine the filtering logic based on the WOT filter option and user state
// when user is not logged in use Site_Only
if (!userState.auth) {
return mods.filter((mod) => isInWoT(siteWot, siteWotLevel, mod.author))
}
// when user is logged, allow other filter selections
const isWoTNpub =
userState.user?.npub === import.meta.env.VITE_SITE_WOT_NPUB
switch (filterOptions.wot) {
case WOTFilterOptions.None:
// Only admins can choose None, use siteWoT for others
return isWoTNpub
? mods
: mods.filter((mod) => isInWoT(siteWot, siteWotLevel, mod.author))
case WOTFilterOptions.Exclude:
// Only admins can choose Exlude, use siteWot for others
// Exlude returns the mods not in the site's WoT
return isWoTNpub
? mods.filter((mod) => !isInWoT(siteWot, siteWotLevel, mod.author))
: mods.filter((mod) => isInWoT(siteWot, siteWotLevel, mod.author))
case WOTFilterOptions.Site_Only:
return mods.filter((mod) =>
isInWoT(siteWot, siteWotLevel, mod.author)
)
case WOTFilterOptions.Mine_Only:
// Only admins can choose Mine_Only, use siteWoT for others
return isWoTNpub
? mods.filter((mod) => isInWoT(userWot, userWotLevel, mod.author))
: mods.filter((mod) => isInWoT(siteWot, siteWotLevel, mod.author))
case WOTFilterOptions.Site_And_Mine:
return mods.filter(
(mod) =>
isInWoT(siteWot, siteWotLevel, mod.author) ||
isInWoT(userWot, userWotLevel, 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 &&
@ -81,13 +129,19 @@ export const useFilteredMods = (
return filtered return filtered
}, [ }, [
userState.auth,
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,
siteWotLevel,
userWot,
userWotLevel
]) ])
} }

View File

@ -10,11 +10,29 @@ const useLocalStorageSubscribe = (callback: () => void) => {
return () => window.removeEventListener('storage', callback) return () => window.removeEventListener('storage', callback)
} }
function mergeWithInitialValue<T>(storedValue: T, initialValue: T): T {
if (typeof storedValue === 'object' && storedValue !== null) {
return { ...initialValue, ...storedValue }
}
return storedValue
}
export function useLocalStorage<T>( export function useLocalStorage<T>(
key: string, key: string,
initialValue: T initialValue: T
): [T, React.Dispatch<React.SetStateAction<T>>] { ): [T, React.Dispatch<React.SetStateAction<T>>] {
const getSnapshot = () => getLocalStorageItem(key, initialValue) const getSnapshot = () => {
// Get the stored value
const storedValue = getLocalStorageItem(key, initialValue)
// Parse the value
const parsedStoredValue = JSON.parse(storedValue)
// Merge the default and the stored in case some of the required fields are missing
return JSON.stringify(
mergeWithInitialValue(parsedStoredValue, initialValue)
)
}
const data = React.useSyncExternalStore(useLocalStorageSubscribe, getSnapshot) const data = React.useSyncExternalStore(useLocalStorageSubscribe, getSnapshot)
@ -35,7 +53,7 @@ export function useLocalStorage<T>(
console.warn(e) console.warn(e)
} }
}, },
[key, data] [data, key]
) )
React.useEffect(() => { React.useEffect(() => {

View File

@ -16,7 +16,7 @@ export const Footer = () => {
by&nbsp; by&nbsp;
<a <a
className={styles.secMainFooterParaLink} className={styles.secMainFooterParaLink}
href='https://primal.net/p/npub18n4ysp43ux5c98fs6h9c57qpr4p8r3j8f6e32v0vj8egzy878aqqyzzk9r' href='https://degmods.com/profile/nprofile1qqsre6jgq6c7r2vzn5cdtju20qq36sn3cer5avc4x8kfru5pzrlr7sqnancjp'
target='_blank' target='_blank'
> >
Freakoverse Freakoverse

View File

@ -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 { resetUserWot } 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(resetUserWot())
} else { } else {
dispatch( dispatch(
setAuth({ setAuth({
@ -223,7 +225,7 @@ export const Header = () => {
> >
<a <a
className={navStyles.NavMainBottomInsideOtherLink} className={navStyles.NavMainBottomInsideOtherLink}
href='https://primal.net/p/npub17jl3ldd6305rnacvwvchx03snauqsg4nz8mruq0emj9thdpglr2sst825x' href='https://degmods.com/profile/nprofile1qqs0f0clkkagh6pe7ux8xvtn8ccf77qgy2e3ra37q8uaez4mks5034gfw4xg6'
target='_blank' target='_blank'
> >
<img <img
@ -379,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

@ -3,13 +3,122 @@ 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 } = 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)
.then((wot) => {
dispatch(setSiteWot(wot))
})
.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, dispatch])
// calculate user's wot
useEffect(() => {
if (ndk && userState.user?.pubkey) {
const hexPubkey = npubToHex(userState.user.pubkey as string)
if (hexPubkey)
calculateWot(hexPubkey, ndk)
.then((wot) => {
dispatch(setUserWot(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, 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'],
authors: [hexPubkey]
},
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'],
authors: [hexPubkey]
},
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 />

View File

@ -22,7 +22,6 @@ export const ReportPopup = ({ handleClose }: ReportPopupProps) => {
useEffect(() => { useEffect(() => {
if (fetcher.data) { if (fetcher.data) {
const { isSent } = fetcher.data const { isSent } = fetcher.data
console.log(fetcher.data)
if (isSent) { if (isSent) {
handleClose() handleClose()
} }

View File

@ -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,
@ -37,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()
@ -247,6 +249,9 @@ const DisplayMod = ({ naddr }: DisplayModProps) => {
const DisplayLatestMods = () => { const DisplayLatestMods = () => {
const navigate = useNavigate() const navigate = useNavigate()
const { fetchMods } = useNDKContext() const { fetchMods } = useNDKContext()
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[]>([])
@ -258,7 +263,12 @@ 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) =>
isInWoT(siteWot, siteWotLevel, mod.author) ||
isInWoT(userWot, userWotLevel, mod.author)
)
setLatestMods(wotFilteredMods)
}) })
.finally(() => { .finally(() => {
setIsFetchingLatestMods(false) setIsFetchingLatestMods(false)

View File

@ -1,124 +1,246 @@
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 { setSiteWotLevel, setUserWotLevel } from 'store/reducers/wot'
import { UserRelaysType } from 'types'
import { log, LogType, now, npubToHex } from 'utils'
// todo: use components from Input.tsx // todo: use components from Input.tsx
export const PreferencesSetting = () => { export const PreferencesSetting = () => {
const { ndk, fetchEventFromUserRelays, publish } = useNDKContext()
const dispatch = useAppDispatch()
const user = useAppSelector((state) => state.user.user)
const { userWotLevel } = useAppSelector((state) => state.wot)
const [wotLevel, setWotLevel] = useState(userWotLevel)
const [isSaving, setIsSaving] = useState(false)
useEffect(() => {
if (user?.pubkey) {
const hexPubkey = user.pubkey as string
fetchEventFromUserRelays(
{
kinds: [NDKKind.AppSpecificData],
'#d': ['degmods'],
authors: [hexPubkey]
},
hexPubkey,
UserRelaysType.Both
).then((event) => {
if (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)
.then((publishedOnRelays) => {
toast.success(
`Preferences published to following relays: \n\n${publishedOnRelays.join(
'\n'
)}`
)
dispatch(setUserWotLevel(wotLevel))
// If wot admin, update site wot level too
const SITE_WOT_NPUB = import.meta.env.VITE_SITE_WOT_NPUB
const siteWotPubkey = npubToHex(SITE_WOT_NPUB)
if (siteWotPubkey === hexPubkey) {
dispatch(setSiteWotLevel(wotLevel))
}
})
.catch((err) => {
console.error(err)
toast.error('Error: Failed to publish preferences!')
})
.finally(() => {
setIsSaving(false)
})
}
return ( return (
<div className='IBMSMSplitMainFullSideFWMid'> <>
<div className='IBMSMSplitMainFullSideSec'> {isSaving && <LoadingSpinner desc='Saving preferences to relays' />}
<div className='IBMSMSMBS_Write'> <div className='IBMSMSplitMainFullSideFWMid'>
<div className='inputLabelWrapperMain'> <div className='IBMSMSplitMainFullSideSec'>
<div className='labelWrapperMain'> <div className='IBMSMSMBS_Write'>
<p className='labelMain'>Notifications</p> <div className='inputLabelWrapperMain'>
<div className='labelWrapperMain'>
<p className='labelMain'>Notifications</p>
</div>
<div className='inputLabelWrapperMain inputLabelWrapperMainAlt inputLabelWrapperMainAltStylized'>
<label className='form-label labelMain'>
When someone follows you
</label>
<input
type='checkbox'
className='CheckboxMain'
name='notificationsSettings'
checked
readOnly
/>
</div>
<div className='inputLabelWrapperMain inputLabelWrapperMainAlt inputLabelWrapperMainAltStylized'>
<label className='form-label labelMain'>
When someone mentions you
</label>
<input
type='checkbox'
className='CheckboxMain'
name='notificationsSettings'
checked
readOnly
/>
</div>
<div className='inputLabelWrapperMain inputLabelWrapperMainAlt inputLabelWrapperMainAltStylized'>
<label className='form-label labelMain'>
When someone sends a reaction to your post
</label>
<input
type='checkbox'
className='CheckboxMain'
name='notificationsSettings'
checked
readOnly
/>
</div>
<div className='inputLabelWrapperMain inputLabelWrapperMainAlt inputLabelWrapperMainAltStylized'>
<label className='form-label labelMain'>
When someone Tips/Zaps you
</label>
<input
type='checkbox'
className='CheckboxMain'
name='notificationsSettings'
checked
readOnly
/>
</div>
<div className='inputLabelWrapperMain inputLabelWrapperMainAlt inputLabelWrapperMainAltStylized'>
<label className='form-label labelMain'>
When someone re-posts your post
</label>
<input
type='checkbox'
className='CheckboxMain'
name='notificationsSettings'
checked
readOnly
/>
</div>
</div> </div>
<div className='inputLabelWrapperMain inputLabelWrapperMainAlt inputLabelWrapperMainAltStylized'> <div className='inputLabelWrapperMain'>
<label className='form-label labelMain'> <div className='labelWrapperMain'>
When someone follows you <p className='labelMain'>Not Safe For Work (NSFW)</p>
</label> </div>
<input <div className='inputLabelWrapperMain inputLabelWrapperMainAlt inputLabelWrapperMainAltStylized'>
type='checkbox' <label className='form-label labelMain'>
className='CheckboxMain' Show all NSFW posts
name='notificationsSettings' </label>
checked <input
/> type='checkbox'
className='CheckboxMain'
name='NSFWPreference'
/>
</div>
</div> </div>
<div className='inputLabelWrapperMain inputLabelWrapperMainAlt inputLabelWrapperMainAltStylized'> <div className='inputLabelWrapperMain'>
<label className='form-label labelMain'> <div className='labelWrapperMain'>
When someone mentions you <p className='labelMain'>Web of Trust (WoT) level</p>
</label> </div>
<input <p className='labelDescriptionMain'>
type='checkbox' This affects what posts you see, reactions, DMs, and
className='CheckboxMain' notifications. Learn more:&nbsp;Link
name='notificationsSettings' </p>
checked <div className='inputLabelWrapperMainSliderWrapper'>
/> <input
className='form-range inputRangeMain inputRangeMainZap'
type='range'
max='100'
min='0'
value={wotLevel}
onChange={(e) => setWotLevel(parseInt(e.target.value))}
step='1'
required
name='WoTLevel'
/>
<p className='ZapSplitUserBoxRangeText'>{wotLevel}</p>
</div>
<div className='inputLabelWrapperMain inputLabelWrapperMainAlt inputLabelWrapperMainAltStylized'>
<label className='form-label labelMain'>
Consider those who zap/tip, regardless of WoT level
</label>
<input
type='checkbox'
className='CheckboxMain'
name='WoTZap'
checked
readOnly
/>
</div>
</div> </div>
<div className='inputLabelWrapperMain inputLabelWrapperMainAlt inputLabelWrapperMainAltStylized'> <div className='IBMSMSMBS_WriteAction'>
<label className='form-label labelMain'> <button
When someone sends a reaction to your post className='btn btnMain'
</label> type='button'
<input onClick={handleSave}
type='checkbox' >
className='CheckboxMain' Save
name='notificationsSettings' </button>
checked
/>
</div> </div>
<div className='inputLabelWrapperMain inputLabelWrapperMainAlt inputLabelWrapperMainAltStylized'>
<label className='form-label labelMain'>
When someone Tips/Zaps you
</label>
<input
type='checkbox'
className='CheckboxMain'
name='notificationsSettings'
checked
/>
</div>
<div className='inputLabelWrapperMain inputLabelWrapperMainAlt inputLabelWrapperMainAltStylized'>
<label className='form-label labelMain'>
When someone re-posts your post
</label>
<input
type='checkbox'
className='CheckboxMain'
name='notificationsSettings'
checked
/>
</div>
</div>
<div className='inputLabelWrapperMain'>
<div className='labelWrapperMain'>
<p className='labelMain'>Not Safe For Work (NSFW)</p>
</div>
<div className='inputLabelWrapperMain inputLabelWrapperMainAlt inputLabelWrapperMainAltStylized'>
<label className='form-label labelMain'>
Show all NSFW posts
</label>
<input
type='checkbox'
className='CheckboxMain'
name='NSFWPreference'
/>
</div>
</div>
<div className='inputLabelWrapperMain'>
<div className='labelWrapperMain'>
<p className='labelMain'>Web of Trust (WoT) level</p>
</div>
<p className='labelDescriptionMain'>
This affects what posts you see, reactions, DMs, and
notifications. Learn more:&nbsp;Link
</p>
<div className='inputLabelWrapperMainSliderWrapper'>
<input
className='form-range inputRangeMain inputRangeMainZap'
type='range'
max='100'
min='0'
value='10'
step='1'
required
name='WoTLevel'
/>
<p className='ZapSplitUserBoxRangeText'>10</p>
</div>
<div className='inputLabelWrapperMain inputLabelWrapperMainAlt inputLabelWrapperMainAltStylized'>
<label className='form-label labelMain'>
Consider those who zap/tip, regardless of WoT level
</label>
<input
type='checkbox'
className='CheckboxMain'
name='WoTZap'
checked
/>
</div>
</div>
<div className='IBMSMSMBS_WriteAction'>
<button className='btn btnMain' type='button'>
Save
</button>
</div> </div>
</div> </div>
</div> </div>
</div> </>
) )
} }

View File

@ -11,7 +11,7 @@ import { Event, kinds, UnsignedEvent } from 'nostr-tools'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { toast } from 'react-toastify' import { toast } from 'react-toastify'
import { UserRelaysType } from 'types' import { UserRelaysType } from 'types'
import { log, LogType, normalizeWebSocketURL, now } from 'utils' import { log, LogType, normalizeWebSocketURL, now, timeout } from 'utils'
const READ_MARKER = 'read' const READ_MARKER = 'read'
const WRITE_MARKER = 'write' const WRITE_MARKER = 'write'
@ -21,12 +21,16 @@ export const RelaySettings = () => {
const userState = useAppSelector((state) => state.user) const userState = useAppSelector((state) => state.user)
const [ndkRelayList, setNDKRelayList] = useState<NDKRelayList | null>(null) const [ndkRelayList, setNDKRelayList] = useState<NDKRelayList | null>(null)
const [isPublishing, setIsPublishing] = useState(false) const [isPublishing, setIsPublishing] = useState(false)
const [isLoading, setIsLoading] = useState(true)
const [inputValue, setInputValue] = useState('') const [inputValue, setInputValue] = useState('')
useEffect(() => { useEffect(() => {
if (userState.auth && userState.user?.pubkey) { if (userState.auth && userState.user?.pubkey) {
getRelayListForUser(userState.user.pubkey as string, ndk) setIsLoading(true)
Promise.race([
getRelayListForUser(userState.user?.pubkey as string, ndk),
timeout(10000)
])
.then((res) => { .then((res) => {
setNDKRelayList(res) setNDKRelayList(res)
}) })
@ -36,9 +40,13 @@ export const RelaySettings = () => {
err.message || err err.message || err
}` }`
) )
setNDKRelayList(null) setNDKRelayList(new NDKRelayList(ndk))
})
.finally(() => {
setIsLoading(false)
}) })
} else { } else {
setIsLoading(false)
setNDKRelayList(null) setNDKRelayList(null)
} }
}, [userState, ndk]) }, [userState, ndk])
@ -224,6 +232,14 @@ export const RelaySettings = () => {
setIsPublishing(false) setIsPublishing(false)
} }
if (isLoading)
return (
<>
<div></div>
<LoadingSpinner desc='Loading' />
</>
)
if (!ndkRelayList) if (!ndkRelayList)
return <div>Could not fetch user relay list or user is not logged in </div> return <div>Could not fetch user relay list or user is not logged in </div>
@ -258,6 +274,12 @@ export const RelaySettings = () => {
<div className='inputLabelWrapperMain'> <div className='inputLabelWrapperMain'>
<label className='form-label labelMain'>Your relays</label> <label className='form-label labelMain'>Your relays</label>
</div> </div>
{relayEntries.length === 0 && (
<>
We recommend adding one of our relays if you're planning to
frequently use DEG Mods, for a better experience.
</>
)}
{relayEntries.map(([relayUrl, relayType]) => ( {relayEntries.map(([relayUrl, relayType]) => (
<RelayListItem <RelayListItem
key={relayUrl} key={relayUrl}

View File

@ -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
} }
}) })

70
src/store/reducers/wot.ts Normal file
View File

@ -0,0 +1,70 @@
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: Record<string, number>
siteWotStatus: WOTStatus
siteWotLevel: number
userWot: Record<string, number>
userWotStatus: WOTStatus
userWotLevel: number
}
const initialState: IWOT = {
siteWot: {},
siteWotStatus: WOTStatus.IDLE,
siteWotLevel: 0,
userWot: {},
userWotStatus: WOTStatus.IDLE,
userWotLevel: 0
}
export const wotSlice = createSlice({
name: 'wot',
initialState,
reducers: {
setSiteWot(state, action: PayloadAction<Record<string, number>>) {
state.siteWot = action.payload
state.siteWotStatus = WOTStatus.LOADED
},
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>) {
state.siteWotStatus = action.payload
},
setUserWotStatus(state, action: PayloadAction<WOTStatus>) {
state.userWotStatus = action.payload
},
setSiteWotLevel(state, action: PayloadAction<number>) {
state.siteWotLevel = action.payload
},
setUserWotLevel(state, action: PayloadAction<number>) {
state.userWotLevel = action.payload
}
}
})
export const {
setSiteWot,
setUserWot,
setSiteWotStatus,
setUserWotStatus,
setSiteWotLevel,
setUserWotLevel,
resetUserWot
} = wotSlice.actions
export default wotSlice.reducer

View File

@ -19,6 +19,7 @@
justify-content: center; justify-content: center;
align-items: center; align-items: center;
padding-top: 56.25%; padding-top: 56.25%;
border-bottom: solid 1px rgb(255 255 255 / 5%);
} }
.IBMSMSMBSSPostTitle { .IBMSMSMBSSPostTitle {
@ -28,6 +29,7 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
color: rgb(255 255 255 / 85%);
} }
.IBMSMSMBSSPostBody { .IBMSMSMBSSPostBody {
@ -38,6 +40,7 @@
align-items: center; align-items: center;
position: relative; position: relative;
overflow: hidden; overflow: hidden;
color: rgb(255 255 255 / 85%);
} }
.IBMSMSMBSSPostBody > div { .IBMSMSMBSSPostBody > div {
@ -238,3 +241,35 @@
border-radius: 6px; border-radius: 6px;
padding: 2px; padding: 2px;
} }
.IBMSMSMBSSPostBody > div > div > p {
margin-bottom: 10px;
}
.IBMSMSMBSSPostBody > div > div > *:is(h1, h2, h3, h4, h5, h6) {
margin: 15px 0 15px 0 !important;
border-bottom: solid 1px rgb(255 255 255 / 10%);
padding: 0px 0 10px 0;
line-height: 1.5 !important;
}
.dropdown.dropdownMain.dropdownMainBlogpost {
flex-grow: unset;
position: absolute;
top: 10px;
right: 10px;
background: rgba(0,0,0,0.1);
border-radius: 6px;
padding: 2px;
}
.IBMSMSMBSSWarning {
width: 100%;
border-radius: 8px;
padding: 10px 15px;
border: solid 2px tomato;
background: rgba(255,80,80,0.15);
color: rgba(255,255,255,0.95);
text-align: center;
}

View File

@ -17,9 +17,18 @@ 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',
Exclude = 'Exclude'
}
export interface FilterOptions { export interface FilterOptions {
sort: SortBy sort: SortBy
nsfw: NSFWFilter nsfw: NSFWFilter
source: string source: string
moderated: ModeratedFilter moderated: ModeratedFilter
wot: WOTFilterOptions
} }

View File

@ -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
} }

135
src/utils/wot.ts Normal file
View File

@ -0,0 +1,135 @@
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) => {
const WoT: Record<string, number> = {}
const userRelations = await findFollowsAndMuteUsers(pubkey, ndk)
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 promises = follows.map((user) =>
findFollowsAndMuteUsers(user, ndk).then((userRelations) => {
network.set(user, userRelations)
})
)
await Promise.all(promises)
// 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))
})
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
}
})
return WoT
}
export const calculateWoTScore = (user: Hexpubkey, network: Network) => {
let wotScore = 0
// 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
if (muted.has(user)) 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)
})
}
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
}
})
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
}

1
src/vite-env.d.ts vendored
View File

@ -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