add experimental Content Discovery DVM tool

This commit is contained in:
hzrd149
2023-11-16 11:29:55 -06:00
parent ba7ac0e321
commit e6bf8d8e95
9 changed files with 255 additions and 8 deletions

View File

@@ -71,6 +71,8 @@ import RelayReviewsView from "./views/relays/reviews";
import PopularRelaysView from "./views/relays/popular";
import UserDMsTab from "./views/user/dms";
import DMFeedView from "./views/tools/dm-feed";
import ContentDiscoveryView from "./views/tools/content-discovery";
import ContentDiscoveryDVMView from "./views/tools/content-discovery/dvm";
const UserTracksTab = lazy(() => import("./views/user/tracks"));
const ToolsHomeView = lazy(() => import("./views/tools"));
@@ -228,6 +230,13 @@ const router = createHashRouter([
path: "tools",
children: [
{ path: "", element: <ToolsHomeView /> },
{
path: "content-discovery",
children: [
{ path: "", element: <ContentDiscoveryView /> },
{ path: ":pubkey", element: <ContentDiscoveryDVMView /> },
],
},
{ path: "network", element: <NetworkView /> },
{ path: "network-mute-graph", element: <NetworkMuteGraphView /> },
{ path: "network-dm-graph", element: <NetworkDMGraphView /> },

View File

@@ -5,16 +5,23 @@ import Subject from "./subject";
export type EventFilter = (event: NostrEvent, store: EventStore) => boolean;
function sortByDate(a: NostrEvent, b: NostrEvent) {
return b.created_at - a.created_at;
}
export default class EventStore {
name?: string;
events = new Map<string, NostrEvent>();
constructor(name?: string) {
customSort?: typeof sortByDate;
constructor(name?: string, customSort?: typeof sortByDate) {
this.name = name;
this.customSort = customSort;
}
getSortedEvents() {
return Array.from(this.events.values()).sort((a, b) => b.created_at - a.created_at);
return Array.from(this.events.values()).sort(this.customSort || sortByDate);
}
onEvent = new Subject<NostrEvent>(undefined, false);

View File

@@ -36,6 +36,7 @@ import { NoteContents } from "../note/text-note-contents";
import Timestamp from "../timestamp";
import { readablizeSats } from "../../helpers/bolt11";
import { LightningIcon } from "../icons";
import { DMV_STATUS_KIND, DMV_TRANSLATE_JOB_KIND, DMV_TRANSLATE_RESULT_KIND } from "../../helpers/nostr/dvm";
function TranslationResult({ result }: { result: NostrEvent }) {
const requester = result.tags.find(isPTag)?.[1];
@@ -66,7 +67,7 @@ function TranslationRequest({ request }: { request: NostrEvent }) {
const readRelays = useReadRelayUrls();
const timeline = useTimelineLoader(`${getEventUID(request)}-offers`, requestRelays || readRelays, {
kinds: [7000],
kinds: [DMV_STATUS_KIND],
"#e": [request.id],
});
@@ -161,7 +162,7 @@ export default function NoteTranslationModal({
try {
const top8Relays = relayScoreboardService.getRankedRelays(readRelays).slice(0, 8);
const draft: DraftNostrEvent = {
kind: 5002,
kind: DMV_TRANSLATE_JOB_KIND,
content: "",
created_at: dayjs().unix(),
tags: [
@@ -180,14 +181,15 @@ export default function NoteTranslationModal({
}, [requestSignature, note, readRelays]);
const timeline = useTimelineLoader(`${getEventUID(note)}-translations`, readRelays, {
kinds: [5002, 6002],
kinds: [DMV_TRANSLATE_JOB_KIND, DMV_TRANSLATE_RESULT_KIND],
"#i": [note.id],
});
const events = useSubject(timeline.timeline);
const filteredEvents = events.filter(
(e, i, arr) =>
e.kind === 6002 || (e.kind === 5002 && !arr.some((r) => r.tags.some((t) => isETag(t) && t[1] === e.id))),
e.kind === DMV_TRANSLATE_RESULT_KIND ||
(e.kind === DMV_TRANSLATE_JOB_KIND && !arr.some((r) => r.tags.some((t) => isETag(t) && t[1] === e.id))),
);
return (
@@ -211,9 +213,9 @@ export default function NoteTranslationModal({
</Flex>
{filteredEvents.map((event) => {
switch (event.kind) {
case 5002:
case DMV_TRANSLATE_JOB_KIND:
return <TranslationRequest key={event.id} request={event} />;
case 6002:
case DMV_TRANSLATE_RESULT_KIND:
return <TranslationResult key={event.id} result={event} />;
}
})}

7
src/helpers/nostr/dvm.ts Normal file
View File

@@ -0,0 +1,7 @@
export const DMV_STATUS_KIND = 7000;
export const DMV_TRANSLATE_JOB_KIND = 5002;
export const DMV_TRANSLATE_RESULT_KIND = 6002;
export const DMV_CONTENT_DISCOVERY_JOB_KIND = 5300;
export const DMV_CONTENT_DISCOVERY_RESULT_KIND = 6300;

View File

@@ -3,11 +3,13 @@ import { useUnmount } from "react-use";
import { NostrRequestFilter } from "../types/nostr-query";
import timelineCacheService from "../services/timeline-cache";
import { EventFilter } from "../classes/timeline-loader";
import { NostrEvent } from "../types/nostr-event";
type Options = {
enabled?: boolean;
eventFilter?: EventFilter;
cursor?: number;
customSort?: (a: NostrEvent, b: NostrEvent) => number;
};
export default function useTimelineLoader(key: string, relays: string[], query: NostrRequestFilter, opts?: Options) {
@@ -27,6 +29,9 @@ export default function useTimelineLoader(key: string, relays: string[], query:
timeline.setCursor(opts.cursor);
}
}, [timeline, opts?.cursor]);
useEffect(() => {
timeline.events.customSort = opts?.customSort;
}, [timeline, opts?.customSort]);
const enabled = opts?.enabled ?? true;
useEffect(() => {

View File

@@ -16,6 +16,7 @@ export type NostrQuery = {
"#e"?: string[];
"#g"?: string[];
"#i"?: string[];
"#k"?: string[];
"#l"?: string[];
"#p"?: string[];
"#r"?: string[];

View File

@@ -0,0 +1,138 @@
import { useCallback, useEffect, useMemo, useState } from "react";
import { Button, Flex, useToast } from "@chakra-ui/react";
import { ChevronLeftIcon } from "@chakra-ui/icons";
import { nip19 } from "nostr-tools";
import dayjs from "dayjs";
import { useNavigate, useParams } from "react-router-dom";
import { isHexKey } from "../../../helpers/nip19";
import useTimelineLoader from "../../../hooks/use-timeline-loader";
import { useReadRelayUrls } from "../../../hooks/use-client-relays";
import { DMV_CONTENT_DISCOVERY_JOB_KIND, DMV_CONTENT_DISCOVERY_RESULT_KIND } from "../../../helpers/nostr/dvm";
import VerticalPageLayout from "../../../components/vertical-page-layout";
import useSubject from "../../../hooks/use-subject";
import useCurrentAccount from "../../../hooks/use-current-account";
import RequireCurrentAccount from "../../../providers/require-current-account";
import { DraftNostrEvent, NostrEvent, Tag, isETag } from "../../../types/nostr-event";
import GenericNoteTimeline from "../../../components/timeline-page/generic-note-timeline";
import { useSigningContext } from "../../../providers/signing-provider";
import NostrPublishAction from "../../../classes/nostr-publish-action";
import clientRelaysService from "../../../services/client-relays";
import { useUserRelays } from "../../../hooks/use-user-relays";
function getResultEvents(result: NostrEvent) {
const parsed = JSON.parse(result.content);
if (!Array.isArray(parsed)) return [];
const tags = parsed as Tag[];
return tags.filter(isETag).map((t) => t[1]);
}
function useDVMPointer() {
const { pubkey } = useParams() as { pubkey: string };
if (isHexKey(pubkey)) return pubkey;
const pointer = nip19.decode(pubkey);
switch (pointer.type) {
case "npub":
return pointer.data as string;
case "nprofile":
const d = pointer.data as nip19.ProfilePointer;
return d.pubkey;
default:
throw new Error(`Unknown type ${pointer.type}`);
}
}
function ResultEvents({ result }: { result: NostrEvent }) {
const readRelays = useReadRelayUrls();
const ids = useMemo(() => getResultEvents(result), [result]);
const customSort = useCallback(
(a: NostrEvent, b: NostrEvent) => {
return ids.indexOf(a.id) - ids.indexOf(b.id);
},
[ids],
);
const timeline = useTimelineLoader(`${result.id}-events`, readRelays, { ids }, { customSort });
return <GenericNoteTimeline timeline={timeline} />;
}
function ContentDiscoveryDVMPage() {
const toast = useToast();
const account = useCurrentAccount()!;
const { requestSignature } = useSigningContext();
const navigate = useNavigate();
const pubkey = useDVMPointer();
const [selected, setSelected] = useState("");
const dvmRelays = useUserRelays(pubkey).map((r) => r.url);
const readRelays = useReadRelayUrls(dvmRelays);
const timeline = useTimelineLoader(`${pubkey}-dvm-results`, readRelays, {
authors: [pubkey],
"#p": [account.pubkey],
kinds: [DMV_CONTENT_DISCOVERY_RESULT_KIND],
});
const results = useSubject(timeline.timeline);
const [requesting, setRequesting] = useState(false);
const requestNew = async () => {
try {
setRequesting(true);
const draft: DraftNostrEvent = {
kind: DMV_CONTENT_DISCOVERY_JOB_KIND,
created_at: dayjs().unix(),
content: "",
tags: [
["p", pubkey],
["relays", ...readRelays],
["output", "text/plain"],
],
};
const signed = await requestSignature(draft);
new NostrPublishAction("Content Discovery", clientRelaysService.getWriteUrls(), signed);
setSelected("");
} catch (e) {
if (e instanceof Error) toast({ status: "error", description: e.message });
}
};
useEffect(() => {
setRequesting(false);
}, [results.length]);
const selectedResult = results.find((r) => r.id === selected);
return (
<VerticalPageLayout>
<Flex gap="2">
<Button leftIcon={<ChevronLeftIcon />} onClick={() => navigate(-1)}>
Back
</Button>
<Button onClick={requestNew} isLoading={requesting} colorScheme="primary">
Request New
</Button>
</Flex>
{selectedResult ? (
<ResultEvents result={selectedResult} />
) : (
results.map((result) => (
<Button key={result.id} onClick={() => setSelected(result.id)}>
Result from {dayjs.unix(result.created_at).fromNow()}
</Button>
))
)}
</VerticalPageLayout>
);
}
export default function ContentDiscoveryDVMView() {
return (
<RequireCurrentAccount>
<ContentDiscoveryDVMPage />
</RequireCurrentAccount>
);
}

View File

@@ -0,0 +1,75 @@
import {
Box,
Button,
Card,
CardBody,
CardHeader,
CardProps,
Flex,
Heading,
LinkBox,
LinkOverlay,
SimpleGrid,
} from "@chakra-ui/react";
import { useNavigate, Link as RouterLink } from "react-router-dom";
import VerticalPageLayout from "../../../components/vertical-page-layout";
import { useReadRelayUrls } from "../../../hooks/use-client-relays";
import useTimelineLoader from "../../../hooks/use-timeline-loader";
import { DMV_CONTENT_DISCOVERY_JOB_KIND } from "../../../helpers/nostr/dvm";
import useSubject from "../../../hooks/use-subject";
import { ChevronLeftIcon } from "@chakra-ui/icons";
import { NostrEvent } from "../../../types/nostr-event";
function DVMCard({ appData, ...props }: Omit<CardProps, "children"> & { appData: NostrEvent }) {
const metadata = JSON.parse(appData.content);
return (
<Card as={LinkBox} {...props}>
<Box
aspectRatio={2 / 1}
backgroundImage={metadata.image || metadata.picture}
backgroundPosition="center"
backgroundRepeat="no-repeat"
backgroundSize="cover"
/>
<CardHeader p="4">
<Heading size="md">
<LinkOverlay as={RouterLink} to={`/tools/content-discovery/${appData.pubkey}`}>
{metadata.name || metadata.display_name}
</LinkOverlay>
</Heading>
</CardHeader>
<CardBody px="4" pb="4" pt="0">
{metadata.about}
</CardBody>
</Card>
);
}
export default function ContentDiscoveryView() {
const navigate = useNavigate();
const readRelays = useReadRelayUrls();
const timeline = useTimelineLoader("content-discovery-dvms", readRelays, {
kinds: [31990],
"#k": [String(DMV_CONTENT_DISCOVERY_JOB_KIND)],
});
const DMVs = useSubject(timeline.timeline);
return (
<VerticalPageLayout>
<Flex>
<Button leftIcon={<ChevronLeftIcon />} onClick={() => navigate(-1)}>
Back
</Button>
</Flex>
<SimpleGrid columns={{ base: 1, md: 2, lg: 3, xl: 4 }}>
{DMVs.map((appData) => (
<DVMCard key={appData.id} appData={appData} maxW="lg" />
))}
</SimpleGrid>
</VerticalPageLayout>
);
}

View File

@@ -12,6 +12,9 @@ export default function ToolsHomeView() {
</Heading>
<Divider />
<Flex wrap="wrap" gap="4">
<Button as={RouterLink} to="/tools/content-discovery">
Content Discovery DVM
</Button>
<Button as={RouterLink} to="/tools/network">
Contact network
</Button>