mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-03-26 17:52:18 +01:00
add network dm graph
This commit is contained in:
parent
5ac4cfcb33
commit
0d00f71882
5
.changeset/pink-apples-battle.md
Normal file
5
.changeset/pink-apples-battle.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
"nostrudel": minor
|
||||
---
|
||||
|
||||
Add network dm graph
|
@ -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 /> },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
@ -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>
|
||||
|
172
src/views/tools/network-dm-graph.tsx
Normal file
172
src/views/tools/network-dm-graph.tsx
Normal 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>
|
||||
);
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user