Skip to content

Commit

Permalink
add network dm graph
Browse files Browse the repository at this point in the history
  • Loading branch information
hzrd149 committed Oct 19, 2023
1 parent 5ac4cfc commit 0d00f71
Show file tree
Hide file tree
Showing 4 changed files with 185 additions and 3 deletions.
5 changes: 5 additions & 0 deletions .changeset/pink-apples-battle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"nostrudel": minor
---

Add network dm graph
6 changes: 4 additions & 2 deletions src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"));
Expand Down Expand Up @@ -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 /> },
],
},
{
Expand Down
5 changes: 4 additions & 1 deletion src/views/tools/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
172 changes: 172 additions & 0 deletions src/views/tools/network-dm-graph.tsx
Original file line number Diff line number Diff line change
@@ -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>
);
}

0 comments on commit 0d00f71

Please sign in to comment.