add network dm graph

This commit is contained in:
hzrd149 2023-10-19 08:44:34 -05:00
parent 5ac4cfcb33
commit 0d00f71882
4 changed files with 185 additions and 3 deletions

View File

@ -0,0 +1,5 @@
---
"nostrudel": minor
---
Add network dm graph

View File

@ -73,7 +73,8 @@ const UserTracksTab = lazy(() => import("./views/user/tracks"));
const ToolsHomeView = lazy(() => import("./views/tools"));
const NetworkView = lazy(() => import("./views/tools/network"));
const StreamModerationView = lazy(() => import("./views/tools/stream-moderation"));
const NetworkGraphView = lazy(() => import("./views/tools/network-mute-graph"));
const NetworkMuteGraphView = lazy(() => import("./views/tools/network-mute-graph"));
const NetworkDMGraphView = lazy(() => import("./views/tools/network-dm-graph"));
const UserStreamsTab = lazy(() => import("./views/user/streams"));
const StreamsView = lazy(() => import("./views/streams"));
@ -224,7 +225,8 @@ const router = createHashRouter([
children: [
{ path: "", element: <ToolsHomeView /> },
{ path: "network", element: <NetworkView /> },
{ path: "network-graph", element: <NetworkGraphView /> },
{ path: "network-mute-graph", element: <NetworkMuteGraphView /> },
{ path: "network-dm-graph", element: <NetworkDMGraphView /> },
],
},
{

View File

@ -15,9 +15,12 @@ export default function ToolsHomeView() {
<Button as={RouterLink} to="/tools/network">
Contact network
</Button>
<Button as={RouterLink} to="/tools/network-graph">
<Button as={RouterLink} to="/tools/network-mute-graph">
Contacts Mute Graph
</Button>
<Button as={RouterLink} to="/tools/network-dm-graph">
Contacts DM Graph
</Button>
<Button as={RouterLink} to="/map" leftIcon={<MapIcon />}>
Map
</Button>

View File

@ -0,0 +1,172 @@
import { useEffect, useMemo, useState } from "react";
import { Box, Flex, Input, Text } from "@chakra-ui/react";
import AutoSizer from "react-virtualized-auto-sizer";
import ForceGraph, { LinkObject, NodeObject } from "react-force-graph-3d";
import { Kind } from "nostr-tools";
import dayjs from "dayjs";
import {
Group,
Mesh,
MeshBasicMaterial,
SRGBColorSpace,
SphereGeometry,
Sprite,
SpriteMaterial,
TextureLoader,
} from "three";
import { useCurrentAccount } from "../../hooks/use-current-account";
import RequireCurrentAccount from "../../providers/require-current-account";
import { useUsersMetadata } from "../../hooks/use-user-network";
import { getPubkeysFromList } from "../../helpers/nostr/lists";
import useUserContactList from "../../hooks/use-user-contact-list";
import { useUserMetadata } from "../../hooks/use-user-metadata";
import EventStore from "../../classes/event-store";
import NostrRequest from "../../classes/nostr-request";
import { isPTag } from "../../types/nostr-event";
import RelaySelectionProvider, { useRelaySelectionContext } from "../../providers/relay-selection-provider";
import RelaySelectionButton from "../../components/relay-selection/relay-selection-button";
import { useDebounce } from "react-use";
import useSubject from "../../hooks/use-subject";
type NodeType = { id: string; image?: string; name?: string };
function NetworkDMGraphPage() {
const account = useCurrentAccount()!;
const { relays } = useRelaySelectionContext();
const contacts = useUserContactList(account.pubkey);
const contactsPubkeys = useMemo(
() => (contacts ? getPubkeysFromList(contacts).map((p) => p.pubkey) : []),
[contacts],
);
const [until, setUntil] = useState(dayjs().unix());
const [since, setSince] = useState(dayjs().subtract(1, "week").unix());
const store = useMemo(() => new EventStore(), []);
const [fetchData] = useDebounce(
() => {
if (!contacts) return;
store.clear();
const request = new NostrRequest(relays);
request.onEvent.subscribe(store.addEvent, store);
request.start({
authors: contactsPubkeys,
kinds: [Kind.EncryptedDirectMessage],
since,
until,
});
},
2 * 1000,
[relays, store, contactsPubkeys, since, until],
);
useEffect(() => {
fetchData();
}, [relays, store, contactsPubkeys, since, until]);
const selfMetadata = useUserMetadata(account.pubkey);
const usersMetadata = useUsersMetadata(contactsPubkeys);
const newEventTrigger = useSubject(store.onEvent);
const graphData = useMemo(() => {
if (store.events.size === 0) return { nodes: [], links: [] };
const nodes: Record<string, NodeObject<NodeType>> = {};
const links: Record<string, LinkObject<NodeType>> = {};
const getOrCreateNode = (pubkey: string) => {
if (!nodes[pubkey]) {
const node: NodeType = {
id: pubkey,
};
const metadata = usersMetadata[pubkey];
if (metadata) {
node.image = metadata.picture;
node.name = metadata.name;
}
nodes[pubkey] = node;
}
return nodes[pubkey];
};
for (const [_, dm] of store.events) {
const author = dm.pubkey;
const receiver = dm.tags.find(isPTag)?.[1];
if (!receiver) continue;
if (contactsPubkeys.includes(receiver) && (contactsPubkeys.includes(author) || author === account.pubkey)) {
const keyA = [author, receiver].join("|");
links[keyA] = { source: getOrCreateNode(author), target: getOrCreateNode(receiver) };
}
}
return { nodes: Object.values(nodes), links: Object.values(links) };
}, [contactsPubkeys, account.pubkey, usersMetadata, selfMetadata, newEventTrigger]);
return (
<Flex direction="column" gap="2" h="full" pt="2">
<Flex gap="2" alignItems="center">
<Input
type="datetime-local"
maxW="sm"
value={dayjs.unix(since).format("YYYY-MM-DDThh:mm")}
onChange={(e) => setSince(dayjs(e.target.value).unix())}
/>
<Text>Showing all direct messages between contacts in the last {dayjs.unix(since).fromNow(true)}</Text>
<RelaySelectionButton ml="auto" />
</Flex>
<Box overflow="hidden" flex={1}>
<AutoSizer>
{({ height, width }) => (
<ForceGraph<NodeType>
graphData={graphData}
enableNodeDrag={false}
width={width}
height={height}
linkDirectionalArrowLength={3.5}
linkDirectionalArrowRelPos={1}
linkCurvature={0.25}
nodeThreeObject={(node: NodeType) => {
if (!node.image) {
return new Mesh(new SphereGeometry(5, 12, 6), new MeshBasicMaterial({ color: 0xaa0f0f }));
}
const group = new Group();
const imgTexture = new TextureLoader().load(node.image);
imgTexture.colorSpace = SRGBColorSpace;
const material = new SpriteMaterial({ map: imgTexture });
const sprite = new Sprite(material);
sprite.scale.set(10, 10, 10);
group.children.push(sprite);
// if (node.name) {
// const text = new SpriteText(node.name, 8, "ffffff");
// text.position.set(0, 0, 16);
// group.children.push(text);
// }
return sprite;
}}
/>
)}
</AutoSizer>
</Box>
</Flex>
);
}
export default function NetworkDMGraphView() {
return (
<RequireCurrentAccount>
<RelaySelectionProvider>
<NetworkDMGraphPage />
</RelaySelectionProvider>
</RequireCurrentAccount>
);
}