diff --git a/src/classes/notifications.ts b/src/classes/notifications.ts index 9b0c1a50f..56b018cae 100644 --- a/src/classes/notifications.ts +++ b/src/classes/notifications.ts @@ -103,6 +103,7 @@ export default class AccountNotifications { for (const event of sorted) { if (!Object.hasOwn(event, typeSymbol)) continue; if (mutedPubkeys.includes(event.pubkey)) continue; + if (event.pubkey === this.pubkey) continue; const e = event as CategorizedEvent; switch (e[typeSymbol]) { diff --git a/src/classes/pubkey-graph.ts b/src/classes/pubkey-graph.ts index eae72c5c3..a85161702 100644 --- a/src/classes/pubkey-graph.ts +++ b/src/classes/pubkey-graph.ts @@ -1,8 +1,11 @@ import { NostrEvent } from "nostr-tools"; +import _throttle from "lodash.throttle"; +import { logger } from "../helpers/debug"; export class PubkeyGraph { /** the pubkey at the center of it all */ root: string; + log = logger.extend("PubkeyGraph"); connections = new Map(); distance = new Map(); @@ -87,6 +90,8 @@ export class PubkeyGraph { return 0; } + throttleCompute = _throttle(this.compute.bind(this), 5_000, { leading: false }); + changed = new Set(); compute() { this.distance.clear(); diff --git a/src/components/magic-textarea.tsx b/src/components/magic-textarea.tsx index c3f25f1dc..703ab0cf7 100644 --- a/src/components/magic-textarea.tsx +++ b/src/components/magic-textarea.tsx @@ -14,7 +14,7 @@ import { Emoji, useContextEmojis } from "../providers/global/emoji-provider"; import { useUserSearchDirectoryContext } from "../providers/global/user-directory-provider"; import UserAvatar from "./user/user-avatar"; import UserDnsIdentity from "./user/user-dns-identity"; -import { getWebOfTrust } from "../services/web-of-trust"; +import { useWebOfTrust } from "../providers/global/web-of-trust-provider"; export type PeopleToken = { pubkey: string; names: string[] }; type Token = Emoji | PeopleToken; @@ -60,6 +60,7 @@ const Loading: ReactTextareaAutocompleteProps< >["loadingComponent"] = ({ data }) =>
Loading
; function useAutocompleteTriggers() { + const webOfTrust = useWebOfTrust(); const emojis = useContextEmojis(); const getDirectory = useUserSearchDirectoryContext(); @@ -77,10 +78,12 @@ function useAutocompleteTriggers() { return matchSorter(dir, token.trim(), { keys: ["names"], sorter: (items) => - getWebOfTrust().sortByDistanceAndConnections( - items.sort((a, b) => b.rank - a.rank), - (i) => i.item.pubkey, - ), + webOfTrust + ? webOfTrust.sortByDistanceAndConnections( + items.sort((a, b) => b.rank - a.rank), + (i) => i.item.pubkey, + ) + : items, }).slice(0, 10); }, component: Item, diff --git a/src/index.tsx b/src/index.tsx index 59d87cc9e..5f26b84ad 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -4,7 +4,6 @@ import { App } from "./app"; import { GlobalProviders } from "./providers/global"; import "./services/user-event-sync"; import "./services/username-search"; -import "./services/web-of-trust"; // setup bitcoin connect import { init, onConnected } from "@getalby/bitcoin-connect-react"; diff --git a/src/providers/global/index.tsx b/src/providers/global/index.tsx index cb40f9658..682b87e1a 100644 --- a/src/providers/global/index.tsx +++ b/src/providers/global/index.tsx @@ -11,6 +11,7 @@ import BreakpointProvider from "./breakpoint-provider"; import DecryptionProvider from "./dycryption-provider"; import DMTimelineProvider from "./dms-provider"; import PublishProvider from "./publish-provider"; +import WebOfTrustProvider from "./web-of-trust-provider"; // Top level providers, should be render as close to the root as possible export const GlobalProviders = ({ children }: { children: React.ReactNode }) => { @@ -30,7 +31,9 @@ export const GlobalProviders = ({ children }: { children: React.ReactNode }) => - {children} + + {children} + diff --git a/src/providers/global/web-of-trust-provider.tsx b/src/providers/global/web-of-trust-provider.tsx new file mode 100644 index 000000000..6b26f8aa0 --- /dev/null +++ b/src/providers/global/web-of-trust-provider.tsx @@ -0,0 +1,77 @@ +import { PropsWithChildren, createContext, useContext, useEffect, useMemo } from "react"; +import { NostrEvent, kinds } from "nostr-tools"; +import _throttle from "lodash.throttle"; + +import { getPubkeysFromList } from "../../helpers/nostr/lists"; +import useCurrentAccount from "../../hooks/use-current-account"; +import { PubkeyGraph } from "../../classes/pubkey-graph"; +import replaceableEventsService from "../../services/replaceable-events"; +import { COMMON_CONTACT_RELAY } from "../../const"; + +export function loadSocialGraph( + graph: PubkeyGraph, + kind: number, + pubkey: string, + relay?: string, + maxLvl = 0, + walked: Set = new Set(), +) { + let newEvents = 0; + + const contacts = replaceableEventsService.requestEvent( + relay ? [relay, COMMON_CONTACT_RELAY] : [COMMON_CONTACT_RELAY], + kind, + pubkey, + ); + + walked.add(pubkey); + + const handleEvent = (event: NostrEvent) => { + graph.handleEvent(event); + newEvents++; + graph.throttleCompute(); + + if (maxLvl > 0) { + for (const person of getPubkeysFromList(event)) { + if (walked.has(person.pubkey)) continue; + + loadSocialGraph(graph, kind, person.pubkey, person.relay, maxLvl - 1, walked); + } + } + }; + + if (contacts.value) { + handleEvent(contacts.value); + } else { + contacts.once((event) => handleEvent(event)); + } +} + +const WebOfTrustContext = createContext(null); + +export function useWebOfTrust() { + return useContext(WebOfTrustContext); +} + +export default function WebOfTrustProvider({ pubkey, children }: PropsWithChildren<{ pubkey?: string }>) { + const account = useCurrentAccount(); + if (account && !pubkey) pubkey = account.pubkey; + + const graph = useMemo(() => { + return pubkey ? new PubkeyGraph(pubkey) : null; + }, [pubkey]); + + // load the graph when it changes + useEffect(() => { + if (!graph) return; + + if (import.meta.env.DEV) { + //@ts-expect-error + window.webOfTrust = graph; + } + + loadSocialGraph(graph, kinds.Contacts, graph.root, undefined, 1); + }, [graph]); + + return {children}; +} diff --git a/src/services/web-of-trust.ts b/src/services/web-of-trust.ts deleted file mode 100644 index 70c0e35cb..000000000 --- a/src/services/web-of-trust.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { NostrEvent, kinds } from "nostr-tools"; -import _throttle from "lodash.throttle"; - -import { PubkeyGraph } from "../classes/pubkey-graph"; -import { COMMON_CONTACT_RELAY } from "../const"; -import { logger } from "../helpers/debug"; -import accountService from "./account"; -import replaceableEventsService from "./replaceable-events"; -import { getPubkeysFromList } from "../helpers/nostr/lists"; - -const log = logger.extend("web-of-trust"); -let webOfTrust = new PubkeyGraph(""); - -let newEvents = 0; -const throttleUpdateWebOfTrust = _throttle(() => { - log("Computing web-of-trust with", newEvents, "new events"); - webOfTrust.compute(); - newEvents = 0; -}, 5_000); - -export function loadSocialGraph( - web: PubkeyGraph, - kind: number, - pubkey: string, - relay?: string, - maxLvl = 0, - walked: Set = new Set(), -) { - const contacts = replaceableEventsService.requestEvent( - relay ? [relay, COMMON_CONTACT_RELAY] : [COMMON_CONTACT_RELAY], - kind, - pubkey, - ); - - walked.add(pubkey); - - const handleEvent = (event: NostrEvent) => { - web.handleEvent(event); - newEvents++; - throttleUpdateWebOfTrust(); - - if (maxLvl > 0) { - for (const person of getPubkeysFromList(event)) { - if (walked.has(person.pubkey)) continue; - - loadSocialGraph(web, kind, person.pubkey, person.relay, maxLvl - 1, walked); - } - } - }; - - if (contacts.value) { - handleEvent(contacts.value); - } else { - contacts.once((event) => handleEvent(event)); - } -} - -accountService.current.subscribe((account) => { - if (!account) return; - - webOfTrust = new PubkeyGraph(account.pubkey); - - if (import.meta.env.DEV) { - //@ts-expect-error - window.webOfTrust = webOfTrust; - } - - loadSocialGraph(webOfTrust, kinds.Contacts, account.pubkey, undefined, 1); -}); - -export function getWebOfTrust() { - return webOfTrust; -} diff --git a/src/views/launchpad/components/search-form.tsx b/src/views/launchpad/components/search-form.tsx index b95f1a673..d94ecee25 100644 --- a/src/views/launchpad/components/search-form.tsx +++ b/src/views/launchpad/components/search-form.tsx @@ -10,7 +10,7 @@ import { useUserSearchDirectoryContext } from "../../../providers/global/user-di import UserAvatar from "../../../components/user/user-avatar"; import UserName from "../../../components/user/user-name"; import KeyboardShortcut from "../../../components/keyboard-shortcut"; -import { getWebOfTrust } from "../../../services/web-of-trust"; +import { useWebOfTrust } from "../../../providers/global/web-of-trust-provider"; function UserOption({ pubkey }: { pubkey: string }) { return ( @@ -22,6 +22,7 @@ function UserOption({ pubkey }: { pubkey: string }) { } export default function SearchForm({ ...props }: Omit) { + const webOfTrust = useWebOfTrust(); const getDirectory = useUserSearchDirectoryContext(); const navigate = useNavigate(); const autoComplete = useDisclosure(); @@ -32,17 +33,18 @@ export default function SearchForm({ ...props }: Omit) { const { value: localUsers = [] } = useAsync(async () => { if (queryThrottle.trim().length < 2) return []; - const webOfTrust = getWebOfTrust(); const dir = await getDirectory(); return matchSorter(dir, queryThrottle.trim(), { keys: ["names"], sorter: (items) => - webOfTrust.sortByDistanceAndConnections( - items.sort((a, b) => b.rank - a.rank), - (i) => i.item.pubkey, - ), + webOfTrust + ? webOfTrust.sortByDistanceAndConnections( + items.sort((a, b) => b.rank - a.rank), + (i) => i.item.pubkey, + ) + : items, }).slice(0, 10); - }, [queryThrottle]); + }, [queryThrottle, webOfTrust]); useEffect(() => { if (localUsers.length > 0 && !autoComplete.isOpen) autoComplete.onOpen(); }, [localUsers, autoComplete.isOpen]); diff --git a/src/views/user/following.tsx b/src/views/user/following.tsx index fb3a77f29..829a9a687 100644 --- a/src/views/user/following.tsx +++ b/src/views/user/following.tsx @@ -5,16 +5,17 @@ import { UserCard } from "./components/user-card"; import { useAdditionalRelayContext } from "../../providers/local/additional-relay-context"; import useUserContactList from "../../hooks/use-user-contact-list"; import { getPubkeysFromList } from "../../helpers/nostr/lists"; -import { getWebOfTrust } from "../../services/web-of-trust"; +import { useWebOfTrust } from "../../providers/global/web-of-trust-provider"; export default function UserFollowingTab() { + const webOfTrust = useWebOfTrust(); const { pubkey } = useOutletContext() as { pubkey: string }; const contextRelays = useAdditionalRelayContext(); const contactsList = useUserContactList(pubkey, contextRelays, { alwaysRequest: true }); const people = contactsList ? getPubkeysFromList(contactsList) : []; - const sorted = getWebOfTrust().sortByDistanceAndConnections(people, (p) => p.pubkey); + const sorted = webOfTrust ? webOfTrust.sortByDistanceAndConnections(people, (p) => p.pubkey) : people; if (!contactsList) return ; diff --git a/src/views/wiki/components/wiki-link.tsx b/src/views/wiki/components/wiki-link.tsx index 293d3989f..bc2992d28 100644 --- a/src/views/wiki/components/wiki-link.tsx +++ b/src/views/wiki/components/wiki-link.tsx @@ -22,10 +22,9 @@ import { Link as RouterLink } from "react-router-dom"; import { useReadRelays } from "../../../hooks/use-client-relays"; import { getPageSummary } from "../../../helpers/nostr/wiki"; import UserName from "../../../components/user/user-name"; -import { getWebOfTrust } from "../../../services/web-of-trust"; -import { getSharableEventAddress } from "../../../helpers/nip19"; import dictionaryService from "../../../services/dictionary"; import useSubject from "../../../hooks/use-subject"; +import { useWebOfTrust } from "../../../providers/global/web-of-trust-provider"; export default function WikiLink({ children, @@ -35,6 +34,7 @@ export default function WikiLink({ topic, ...props }: LinkProps & ExtraProps & { maxVersions?: number; topic?: string }) { + const webOfTrust = useWebOfTrust(); const { isOpen, onClose, onOpen } = useDisclosure(); const readRelays = useReadRelays(); @@ -51,7 +51,10 @@ export default function WikiLink({ const sorted = useMemo(() => { if (!events) return []; - const arr = getWebOfTrust().sortByDistanceAndConnections(Array.from(events.values()), (e) => e.pubkey); + + let arr = Array.from(events.values()); + if (webOfTrust) arr = webOfTrust.sortByDistanceAndConnections(arr, (e) => e.pubkey); + const seen = new Set(); const unique: NostrEvent[] = []; @@ -65,15 +68,19 @@ export default function WikiLink({ } return unique; - }, [events]); - - // if there is only one result, redirect to it - const to = sorted?.length === 1 ? "/wiki/page/" + getSharableEventAddress(sorted[0]) : "/wiki/topic/" + topic; + }, [events, maxVersions, webOfTrust]); return ( - + {children} diff --git a/src/views/wiki/create.tsx b/src/views/wiki/create.tsx index 852b2f355..40b7b7aeb 100644 --- a/src/views/wiki/create.tsx +++ b/src/views/wiki/create.tsx @@ -1,35 +1,32 @@ -import { useMemo, useRef, useState } from "react"; -import { Button, Flex, FormControl, FormLabel, Heading, Input, VisuallyHidden, useToast } from "@chakra-ui/react"; -import SimpleMDE, { SimpleMDEReactProps } from "react-simplemde-editor"; +import { + Button, + Flex, + FormControl, + FormHelperText, + FormLabel, + Heading, + Input, + Textarea, + useToast, +} from "@chakra-ui/react"; import { useNavigate, useSearchParams } from "react-router-dom"; -import ReactDOMServer from "react-dom/server"; import { useForm } from "react-hook-form"; import { EventTemplate } from "nostr-tools"; import dayjs from "dayjs"; -import EasyMDE from "easymde"; import "easymde/dist/easymde.min.css"; +import { WIKI_RELAYS } from "../../const"; import VerticalPageLayout from "../../components/vertical-page-layout"; import { removeNonASCIIChar } from "../../helpers/string"; import { usePublishEvent } from "../../providers/global/publish-provider"; import { WIKI_PAGE_KIND } from "../../helpers/nostr/wiki"; import { getSharableEventAddress } from "../../helpers/nip19"; -import { WIKI_RELAYS } from "../../const"; -import useAppSettings from "../../hooks/use-app-settings"; -import { uploadFileToServers } from "../../helpers/media-upload/blossom"; -import useUsersMediaServers from "../../hooks/use-user-media-servers"; -import { useSigningContext } from "../../providers/global/signing-provider"; -import useCurrentAccount from "../../hooks/use-current-account"; import useCacheForm from "../../hooks/use-cache-form"; import MarkdownEditor from "./components/markdown-editor"; export default function CreateWikiPageView() { - const account = useCurrentAccount(); - const { mediaUploadService } = useAppSettings(); - const { servers } = useUsersMediaServers(account?.pubkey); const toast = useToast(); - const { requestSignature } = useSigningContext(); const publish = usePublishEvent(); const navigate = useNavigate(); const [search] = useSearchParams(); @@ -37,7 +34,7 @@ export default function CreateWikiPageView() { const presetTitle = search.get("title"); const { register, setValue, getValues, handleSubmit, watch, formState, reset } = useForm({ - defaultValues: { content: "", title: presetTitle || presetTopic || "", topic: presetTopic || "" }, + defaultValues: { content: "", title: presetTitle || presetTopic || "", topic: presetTopic || "", summary: "" }, mode: "all", }); @@ -99,6 +96,11 @@ export default function CreateWikiPageView() { + + Summary +