diff --git a/.env.example b/.env.example index 83ddd81..7c6fe65 100644 --- a/.env.example +++ b/.env.example @@ -7,6 +7,9 @@ VITE_ADMIN_NPUBS= # A dedicated npub used for reporting mods, blogs, profile and etc. VITE_REPORTING_NPUB= +# A dedicated npub used for site WOT. +VITE_SITE_WOT_NPUB= + # 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 diff --git a/.gitea/workflows/release-production.yaml b/.gitea/workflows/release-production.yaml index b2ba757..4a19f05 100644 --- a/.gitea/workflows/release-production.yaml +++ b/.gitea/workflows/release-production.yaml @@ -25,10 +25,10 @@ jobs: echo "VITE_APP_RELAY=${{ vars.VITE_APP_RELAY }}" >> .env echo "VITE_ADMIN_NPUBS=${{ vars.VITE_ADMIN_NPUBS }}" >> .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_GAME_IMAGE=${{ vars.VITE_FALLBACK_GAME_IMAGE }}" >> .env echo "VITE_BLOG_NPUBS=${{ vars.VITE_BLOG_NPUBS }}" >> .env - echo "VITE_SITE_WOT_NPUB=${{ vars.VITE_SITE_WOT_NPUB }}" >> .env cat .env - name: Create Build diff --git a/.gitea/workflows/release-staging.yaml b/.gitea/workflows/release-staging.yaml index 683d4d1..aa2c833 100644 --- a/.gitea/workflows/release-staging.yaml +++ b/.gitea/workflows/release-staging.yaml @@ -25,10 +25,10 @@ jobs: echo "VITE_APP_RELAY=${{ vars.VITE_APP_RELAY }}" >> .env echo "VITE_ADMIN_NPUBS=${{ vars.VITE_ADMIN_NPUBS }}" >> .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_GAME_IMAGE=${{ vars.VITE_FALLBACK_GAME_IMAGE }}" >> .env echo "VITE_BLOG_NPUBS=${{ vars.VITE_BLOG_NPUBS }}" >> .env - echo "VITE_SITE_WOT_NPUB=${{ vars.VITE_SITE_WOT_NPUB }}" >> .env cat .env - name: Create Build diff --git a/.github/workflows/release-pages-production.yaml b/.github/workflows/release-pages-production.yaml index 93cdf1e..0536394 100644 --- a/.github/workflows/release-pages-production.yaml +++ b/.github/workflows/release-pages-production.yaml @@ -32,11 +32,11 @@ jobs: run: | echo "VITE_APP_RELAY=${{ vars.VITE_APP_RELAY }}" >> .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_MOD_IMAGE=${{ vars.VITE_FALLBACK_MOD_IMAGE }}" >> .env echo "VITE_REPORTING_NPUB=${{ vars.VITE_REPORTING_NPUB }}" >> .env echo "VITE_BLOG_NPUBS=${{ vars.VITE_BLOG_NPUBS }}" >> .env - echo "VITE_SITE_WOT_NPUB=${{ vars.VITE_SITE_WOT_NPUB }}" >> .env cat .env - name: Build run: npm run build diff --git a/src/assets/games/Games_Other.csv b/src/assets/games/Games_Other.csv index d32146e..403b61f 100644 --- a/src/assets/games/Games_Other.csv +++ b/src/assets/games/Games_Other.csv @@ -1,5 +1,7 @@ -Game Name,16 by 9 image,Boxart image -(Unlisted Game),, -Minecraft,,https://image.nostr.build/b75b2d3a7855370230f2976567e2d5f913a567c57ac61adfb60c7e1102f05117.jpg -Vintage Story,,https://image.nostr.build/9efe683d339cc864032a99047ce26b2b5c19fab1ec4dcc6d4db96e2785c44eda.png -Yandere Simulator,,https://image.nostr.build/54ba56b752bb9d411cbdc1d249fa0cb74c6062a305bcd0a70ecacb61b8d50030.png \ No newline at end of file +Game Name,16 by 9 image,Boxart image +(Unlisted Game),, +Minecraft,,https://image.nostr.build/b75b2d3a7855370230f2976567e2d5f913a567c57ac61adfb60c7e1102f05117.jpg +Vintage Story,,https://image.nostr.build/9efe683d339cc864032a99047ce26b2b5c19fab1ec4dcc6d4db96e2785c44eda.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 \ No newline at end of file diff --git a/src/assets/games/Games_Steam3.csv b/src/assets/games/Games_Steam3.csv index 9592d26..23043ab 100644 --- a/src/assets/games/Games_Steam3.csv +++ b/src/assets/games/Games_Steam3.csv @@ -24966,7 +24966,7 @@ Mystery of Island,, SEARCH ALL - POTIONS,, Sanctuary Saga Playtest,, Fantasy Grounds - Beastheart and Monstrous Companions,, -Dragon Ageā„¢: The Veilguard,, +Dragon Age: The Veilguard,,https://image.nostr.build/0ec48cc2ef08b3f09647c8403e29c5fd814dc3e4aeb2bb76add93928f3a867b0.jpg Transport INC - Map Pack,, Fantasy Grounds - Dungeon Crawl Classics #97: The Queen of Elfland's Son,, cyberpunkdreams: outside edges,, diff --git a/src/components/ModsFilter.tsx b/src/components/ModsFilter.tsx index 292f3a6..8d733c7 100644 --- a/src/components/ModsFilter.tsx +++ b/src/components/ModsFilter.tsx @@ -1,6 +1,12 @@ import { useAppSelector, useLocalStorage } from 'hooks' 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' type Props = { @@ -19,6 +25,7 @@ export const ModFilter = React.memo( return (
+ {/* sort filter options */}
+ + {/* moderation filter options */}
+ + {/* wot filter options */} +
+
+ +
+ {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 ( +
+ setFilterOptions((prev) => ({ + ...prev, + wot: item + })) + } + > + {item} +
+ ) + })} +
+
+
+ + {/* nsfw filter options */}
+ + {/* source filter options */}
-
- - -
-
- - -
-
-
-
-

Not Safe For Work (NSFW)

-
-
- - -
-
-
-
-

Web of Trust (WoT) level

-
-

- This affects what posts you see, reactions, DMs, and - notifications. Learn more: Link -

-
- -

10

-
-
- - -
-
-
-
- + ) } diff --git a/src/pages/settings/relay.tsx b/src/pages/settings/relay.tsx index 1a32d28..89f0957 100644 --- a/src/pages/settings/relay.tsx +++ b/src/pages/settings/relay.tsx @@ -11,7 +11,7 @@ import { Event, kinds, UnsignedEvent } from 'nostr-tools' import { useEffect, useState } from 'react' import { toast } from 'react-toastify' import { UserRelaysType } from 'types' -import { log, LogType, normalizeWebSocketURL, now } from 'utils' +import { log, LogType, normalizeWebSocketURL, now, timeout } from 'utils' const READ_MARKER = 'read' const WRITE_MARKER = 'write' @@ -21,12 +21,16 @@ export const RelaySettings = () => { const userState = useAppSelector((state) => state.user) const [ndkRelayList, setNDKRelayList] = useState(null) const [isPublishing, setIsPublishing] = useState(false) - + const [isLoading, setIsLoading] = useState(true) const [inputValue, setInputValue] = useState('') useEffect(() => { 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) => { setNDKRelayList(res) }) @@ -36,9 +40,13 @@ export const RelaySettings = () => { err.message || err }` ) - setNDKRelayList(null) + setNDKRelayList(new NDKRelayList(ndk)) + }) + .finally(() => { + setIsLoading(false) }) } else { + setIsLoading(false) setNDKRelayList(null) } }, [userState, ndk]) @@ -224,6 +232,14 @@ export const RelaySettings = () => { setIsPublishing(false) } + if (isLoading) + return ( + <> +
+ + + ) + if (!ndkRelayList) return
Could not fetch user relay list or user is not logged in
@@ -258,6 +274,12 @@ export const RelaySettings = () => {
+ {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]) => ( + siteWotStatus: WOTStatus + siteWotLevel: number + userWot: Record + 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>) { + state.siteWot = action.payload + state.siteWotStatus = WOTStatus.LOADED + }, + setUserWot(state, action: PayloadAction>) { + state.userWot = action.payload + state.userWotStatus = WOTStatus.LOADED + }, + resetUserWot(state) { + state.userWot = {} + state.userWotStatus = WOTStatus.IDLE + state.userWotLevel = 0 + }, + setSiteWotStatus(state, action: PayloadAction) { + state.siteWotStatus = action.payload + }, + setUserWotStatus(state, action: PayloadAction) { + state.userWotStatus = action.payload + }, + setSiteWotLevel(state, action: PayloadAction) { + state.siteWotLevel = action.payload + }, + setUserWotLevel(state, action: PayloadAction) { + state.userWotLevel = action.payload + } + } +}) + +export const { + setSiteWot, + setUserWot, + setSiteWotStatus, + setUserWotStatus, + setSiteWotLevel, + setUserWotLevel, + resetUserWot +} = wotSlice.actions + +export default wotSlice.reducer diff --git a/src/styles/post.css b/src/styles/post.css index 746f823..1792296 100644 --- a/src/styles/post.css +++ b/src/styles/post.css @@ -19,6 +19,7 @@ justify-content: center; align-items: center; padding-top: 56.25%; + border-bottom: solid 1px rgb(255 255 255 / 5%); } .IBMSMSMBSSPostTitle { @@ -28,6 +29,7 @@ display: flex; flex-direction: column; align-items: center; + color: rgb(255 255 255 / 85%); } .IBMSMSMBSSPostBody { @@ -38,6 +40,7 @@ align-items: center; position: relative; overflow: hidden; + color: rgb(255 255 255 / 85%); } .IBMSMSMBSSPostBody > div { @@ -237,4 +240,36 @@ background: #0000002e; border-radius: 6px; padding: 2px; -} \ No newline at end of file +} + +.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; +} + diff --git a/src/types/modsFilter.ts b/src/types/modsFilter.ts index 8d724ec..b0ae63b 100644 --- a/src/types/modsFilter.ts +++ b/src/types/modsFilter.ts @@ -17,9 +17,18 @@ export enum ModeratedFilter { 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 { sort: SortBy nsfw: NSFWFilter source: string moderated: ModeratedFilter + wot: WOTFilterOptions } diff --git a/src/utils/consts.ts b/src/utils/consts.ts index ff8e47b..611a3a8 100644 --- a/src/utils/consts.ts +++ b/src/utils/consts.ts @@ -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 = { sort: SortBy.Latest, nsfw: NSFWFilter.Hide_NSFW, source: window.location.host, - moderated: ModeratedFilter.Moderated + moderated: ModeratedFilter.Moderated, + wot: WOTFilterOptions.Site_Only } diff --git a/src/utils/wot.ts b/src/utils/wot.ts new file mode 100644 index 0000000..fa8bcd0 --- /dev/null +++ b/src/utils/wot.ts @@ -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 + muted: Set +} + +type Network = Map + +export const calculateWot = async (pubkey: Hexpubkey, ndk: NDK) => { + const WoT: Record = {} + + 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() + 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 => { + const follows = new Set() + const muted = new Set() + + 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, + targetScore: number, + targetUser: string +): boolean => { + const wotScore = WoT[targetUser] ?? 0 // Default to 0 if the user is not in the record + return wotScore >= targetScore +} diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts index 6363e3e..b4b6321 100644 --- a/src/vite-env.d.ts +++ b/src/vite-env.d.ts @@ -4,6 +4,7 @@ interface ImportMetaEnv { readonly VITE_APP_RELAY: string readonly VITE_ADMIN_NPUBS: string readonly VITE_REPORTING_NPUB: string + readonly VITE_SITE_WOT_NPUB: string readonly VITE_FALLBACK_MOD_IMAGE: string readonly VITE_FALLBACK_GAME_IMAGE: string readonly VITE_BLOG_NPUBS: string