mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-06-11 09:21:29 +02: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 ToolsHomeView = lazy(() => import("./views/tools"));
|
||||||
const NetworkView = lazy(() => import("./views/tools/network"));
|
const NetworkView = lazy(() => import("./views/tools/network"));
|
||||||
const StreamModerationView = lazy(() => import("./views/tools/stream-moderation"));
|
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 UserStreamsTab = lazy(() => import("./views/user/streams"));
|
||||||
const StreamsView = lazy(() => import("./views/streams"));
|
const StreamsView = lazy(() => import("./views/streams"));
|
||||||
@ -224,7 +225,8 @@ const router = createHashRouter([
|
|||||||
children: [
|
children: [
|
||||||
{ path: "", element: <ToolsHomeView /> },
|
{ path: "", element: <ToolsHomeView /> },
|
||||||
{ path: "network", element: <NetworkView /> },
|
{ 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">
|
<Button as={RouterLink} to="/tools/network">
|
||||||
Contact network
|
Contact network
|
||||||
</Button>
|
</Button>
|
||||||
<Button as={RouterLink} to="/tools/network-graph">
|
<Button as={RouterLink} to="/tools/network-mute-graph">
|
||||||
Contacts Mute Graph
|
Contacts Mute Graph
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button as={RouterLink} to="/tools/network-dm-graph">
|
||||||
|
Contacts DM Graph
|
||||||
|
</Button>
|
||||||
<Button as={RouterLink} to="/map" leftIcon={<MapIcon />}>
|
<Button as={RouterLink} to="/map" leftIcon={<MapIcon />}>
|
||||||
Map
|
Map
|
||||||
</Button>
|
</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