fixes for web of trust

This commit is contained in:
hzrd149 2024-05-16 16:12:38 -05:00
parent 36db83d3d7
commit fb3fd90224
14 changed files with 155 additions and 134 deletions

View File

@ -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]) {

View File

@ -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<string, string[]>();
distance = new Map<string, number>();
@ -87,6 +90,8 @@ export class PubkeyGraph {
return 0;
}
throttleCompute = _throttle(this.compute.bind(this), 5_000, { leading: false });
changed = new Set<string>();
compute() {
this.distance.clear();

View File

@ -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 }) => <div>Loading</div>;
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,

View File

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

View File

@ -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 }) =>
<DMTimelineProvider>
<DefaultEmojiProvider>
<UserEmojiProvider>
<AllUserSearchDirectoryProvider>{children}</AllUserSearchDirectoryProvider>
<AllUserSearchDirectoryProvider>
<WebOfTrustProvider>{children}</WebOfTrustProvider>
</AllUserSearchDirectoryProvider>
</UserEmojiProvider>
</DefaultEmojiProvider>
</DMTimelineProvider>

View File

@ -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<string> = 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<PubkeyGraph | null>(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 <WebOfTrustContext.Provider value={graph}>{children}</WebOfTrustContext.Provider>;
}

View File

@ -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<string> = 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;
}

View File

@ -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<FlexProps, "children">) {
const webOfTrust = useWebOfTrust();
const getDirectory = useUserSearchDirectoryContext();
const navigate = useNavigate();
const autoComplete = useDisclosure();
@ -32,17 +33,18 @@ export default function SearchForm({ ...props }: Omit<FlexProps, "children">) {
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]);

View File

@ -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 <Spinner />;

View File

@ -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<string>();
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 (
<Popover returnFocusOnClose={false} isOpen={isOpen} onClose={onClose} placement="top" closeOnBlur={true}>
<PopoverTrigger>
<Link as={RouterLink} color="blue.500" {...props} to={to} onMouseEnter={onOpen} onMouseLeave={onClose}>
<Link
as={RouterLink}
color="blue.500"
{...props}
to={"/wiki/topic/" + topic}
onMouseEnter={onOpen}
onMouseLeave={onClose}
>
{children}
</Link>
</PopoverTrigger>

View File

@ -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() {
<Input {...register("title", { required: true })} autoComplete="off" />
</FormControl>
</Flex>
<FormControl>
<FormLabel>Summary</FormLabel>
<Textarea {...register("summary", { required: true })} isRequired />
<FormHelperText>We'll never share your email.</FormHelperText>
</FormControl>
<MarkdownEditor value={getValues().content} onChange={(v) => setValue("content", v)} />
<Flex gap="2" justifyContent="flex-end">
<Button onClick={() => navigate(-1)}>Cancel</Button>

View File

@ -21,7 +21,6 @@ import VerticalPageLayout from "../../components/vertical-page-layout";
import { getPageDefer, getPageForks, getPageTitle, getPageTopic } from "../../helpers/nostr/wiki";
import MarkdownContent from "./components/markdown";
import UserLink from "../../components/user/user-link";
import { getWebOfTrust } from "../../services/web-of-trust";
import useSubject from "../../hooks/use-subject";
import WikiPageResult from "./components/wiki-page-result";
import Timestamp from "../../components/timestamp";
@ -38,6 +37,7 @@ import EventVoteButtons from "../../components/reactions/event-vote-buttions";
import useCurrentAccount from "../../hooks/use-current-account";
import dictionaryService from "../../services/dictionary";
import { useReadRelays } from "../../hooks/use-client-relays";
import { useWebOfTrust } from "../../providers/global/web-of-trust-provider";
function ForkAlert({ page, address }: { page: NostrEvent; address: nip19.AddressPointer }) {
const topic = getPageTopic(page);
@ -91,8 +91,6 @@ export function WikiPagePage({ page }: { page: NostrEvent }) {
const topic = getPageTopic(page);
const readRelays = useReadRelays();
const subject = useMemo(() => dictionaryService.requestTopic(topic, readRelays), [topic, readRelays]);
const pages = useSubject(subject);
const { address } = getPageForks(page);
const defer = getPageDefer(page);
@ -133,24 +131,18 @@ export function WikiPagePage({ page }: { page: NostrEvent }) {
}
function WikiPageFooter({ page }: { page: NostrEvent }) {
const webOfTrust = useWebOfTrust();
const topic = getPageTopic(page);
const readRelays = useReadRelays();
const subject = useMemo(() => dictionaryService.requestTopic(topic, readRelays), [topic, readRelays]);
const pages = useSubject(subject);
const forks = pages
? getWebOfTrust().sortByDistanceAndConnections(
Array.from(pages.values()).filter((p) => getPageForks(p).address?.pubkey === page.pubkey),
(p) => p.pubkey,
)
: [];
const other = pages
? getWebOfTrust().sortByDistanceAndConnections(
Array.from(pages.values()).filter((p) => !forks.includes(p) && p.pubkey !== page.pubkey),
(p) => p.pubkey,
)
: [];
let forks = pages ? Array.from(pages.values()).filter((p) => getPageForks(p).address?.pubkey === page.pubkey) : [];
if (webOfTrust) forks = webOfTrust.sortByDistanceAndConnections(forks, (p) => p.pubkey);
let other = pages ? Array.from(pages.values()).filter((p) => !forks.includes(p) && p.pubkey !== page.pubkey) : [];
if (webOfTrust) other = webOfTrust.sortByDistanceAndConnections(other, (p) => p.pubkey);
return (
<>

View File

@ -12,11 +12,12 @@ import { subscribeMany } from "../../helpers/relay";
import { SEARCH_RELAYS, WIKI_RELAYS } from "../../const";
import { WIKI_PAGE_KIND } from "../../helpers/nostr/wiki";
import { localRelay } from "../../services/local-relay";
import { getWebOfTrust } from "../../services/web-of-trust";
import WikiPageResult from "./components/wiki-page-result";
import dictionaryService from "../../services/dictionary";
import { useWebOfTrust } from "../../providers/global/web-of-trust-provider";
export default function WikiSearchView() {
const webOfTrust = useWebOfTrust();
const { value: query, setValue: setQuery } = useRouteSearchValue("q");
if (!query) return <Navigate to="/wiki" />;
@ -53,7 +54,7 @@ export default function WikiSearchView() {
}
}, [query, setResults]);
const sorted = getWebOfTrust().sortByDistanceAndConnections(results, (p) => p.pubkey);
const sorted = webOfTrust ? webOfTrust.sortByDistanceAndConnections(results, (p) => p.pubkey) : results;
return (
<VerticalPageLayout>

View File

@ -4,7 +4,6 @@ import { NostrEvent } from "nostr-tools";
import VerticalPageLayout from "../../components/vertical-page-layout";
import useSubject from "../../hooks/use-subject";
import { getWebOfTrust } from "../../services/web-of-trust";
import { useEffect, useMemo, useState } from "react";
import dictionaryService from "../../services/dictionary";
import { useReadRelays } from "../../hooks/use-client-relays";
@ -12,18 +11,20 @@ import WikiPageHeader from "./components/wiki-page-header";
import UserAvatar from "../../components/user/user-avatar";
import UserName from "../../components/user/user-name";
import { WikiPagePage } from "./page";
import { useWebOfTrust } from "../../providers/global/web-of-trust-provider";
export default function WikiTopicView() {
const { topic } = useParams();
if (!topic) return <Navigate to="/wiki" />;
const webOfTrust = useWebOfTrust();
const readRelays = useReadRelays();
const subject = useMemo(() => dictionaryService.requestTopic(topic, readRelays), [topic, readRelays]);
const pages = useSubject(subject);
const sorted = pages
? getWebOfTrust().sortByDistanceAndConnections(Array.from(pages?.values()), (p) => p.pubkey)
: [];
let sorted = pages ? Array.from(pages.values()) : [];
if (webOfTrust) sorted = webOfTrust.sortByDistanceAndConnections(sorted, (p) => p.pubkey);
const [selected, setSelected] = useState<NostrEvent>();