refactor: use nostr-social-graph
This commit is contained in:
@ -35,7 +35,7 @@
|
||||
"list": "naddr1qq4xc6tnw3ez6vp58y6rywpjxckngdtyxukngwr9vckkze33vcknzcnrxcenje35xqmn2cczyp3lucccm3v9s087z6qslpkap8schltk427zfgqgrn3g2menq5zw6qcyqqq82vqprpmhxue69uhhyetvv9ujuumwdae8gtnnda3kjctv7rajfl"
|
||||
},
|
||||
"noteCreatorToast": false,
|
||||
"hideFromNavbar": ["/graph"],
|
||||
"hideFromNavbar": [],
|
||||
"deckSubKind": 1,
|
||||
"showPowIcon": true,
|
||||
"eventLinkPrefix": "nevent",
|
||||
|
@ -34,7 +34,7 @@
|
||||
},
|
||||
"communityLeaders": null,
|
||||
"noteCreatorToast": false,
|
||||
"hideFromNavbar": ["/graph"],
|
||||
"hideFromNavbar": [],
|
||||
"deckSubKind": 1,
|
||||
"showPowIcon": true,
|
||||
"eventLinkPrefix": "nevent",
|
||||
|
@ -33,7 +33,7 @@
|
||||
},
|
||||
"communityLeaders": null,
|
||||
"noteCreatorToast": true,
|
||||
"hideFromNavbar": ["/graph"],
|
||||
"hideFromNavbar": [],
|
||||
"deckSubKind": 1,
|
||||
"showPowIcon": true,
|
||||
"eventLinkPrefix": "nevent",
|
||||
|
@ -1,15 +1,16 @@
|
||||
import { HexKey, socialGraphInstance } from "@snort/system";
|
||||
import classNames from "classnames";
|
||||
|
||||
import Icon from "@/Components/Icons/Icon";
|
||||
import useWoT from "@/Hooks/useWoT";
|
||||
|
||||
interface FollowDistanceIndicatorProps {
|
||||
pubkey: HexKey;
|
||||
pubkey: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export default function FollowDistanceIndicator({ pubkey, className }: FollowDistanceIndicatorProps) {
|
||||
const followDistance = socialGraphInstance.getFollowDistance(pubkey);
|
||||
const wot = useWoT();
|
||||
const followDistance = wot.followDistance(pubkey);
|
||||
let followDistanceColor = "";
|
||||
let title = "";
|
||||
|
||||
@ -20,7 +21,7 @@ export default function FollowDistanceIndicator({ pubkey, className }: FollowDis
|
||||
followDistanceColor = "success";
|
||||
title = "Following";
|
||||
} else if (followDistance === 2) {
|
||||
const followedByFriendsCount = socialGraphInstance.followedByFriendsCount(pubkey);
|
||||
const followedByFriendsCount = wot.followedByCount(pubkey);
|
||||
if (followedByFriendsCount > 10) {
|
||||
followDistanceColor = "text-nostr-orange";
|
||||
}
|
||||
|
@ -1,17 +1,19 @@
|
||||
import { HexKey, socialGraphInstance } from "@snort/system";
|
||||
import { HexKey } from "@snort/system";
|
||||
import React, { Fragment, useMemo } from "react";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
|
||||
import { AvatarGroup } from "@/Components/User/AvatarGroup";
|
||||
import DisplayName from "@/Components/User/DisplayName";
|
||||
import { ProfileLink } from "@/Components/User/ProfileLink";
|
||||
import useWoT from "@/Hooks/useWoT";
|
||||
|
||||
const MAX_FOLLOWED_BY_FRIENDS = 3;
|
||||
|
||||
export default function FollowedBy({ pubkey }: { pubkey: HexKey }) {
|
||||
const followDistance = socialGraphInstance.getFollowDistance(pubkey);
|
||||
const wot = useWoT();
|
||||
const followDistance = wot.followDistance(pubkey);
|
||||
const { followedByFriendsArray, totalFollowedByFriends } = useMemo(() => {
|
||||
const followedByFriends = socialGraphInstance.followedByFriends(pubkey);
|
||||
const followedByFriends = wot.followedByCount(pubkey);
|
||||
return {
|
||||
followedByFriendsArray: Array.from(followedByFriends).slice(0, MAX_FOLLOWED_BY_FRIENDS),
|
||||
totalFollowedByFriends: followedByFriends.size,
|
||||
|
@ -1,9 +1,7 @@
|
||||
import { CachedMetadata, NostrLink, NostrPrefix, UserMetadata } from "@snort/system";
|
||||
import { SnortContext } from "@snort/system-react";
|
||||
import { ReactNode, useContext } from "react";
|
||||
import { CachedMetadata, UserMetadata } from "@snort/system";
|
||||
import { ReactNode } from "react";
|
||||
import { Link, LinkProps } from "react-router-dom";
|
||||
|
||||
import { randomSample } from "@/Utils";
|
||||
import { useProfileLink } from "@/Hooks/useProfileLink";
|
||||
|
||||
export function ProfileLink({
|
||||
|
@ -1,8 +1,11 @@
|
||||
import { EventKind, HexKey, RequestBuilder, socialGraphInstance } from "@snort/system";
|
||||
import { EventKind, HexKey, RequestBuilder } from "@snort/system";
|
||||
import { useRequestBuilder } from "@snort/system-react";
|
||||
import { useMemo } from "react";
|
||||
|
||||
import useWoT from "@/Hooks/useWoT";
|
||||
|
||||
export default function useFollowersFeed(pubkey?: HexKey) {
|
||||
const wot = useWoT();
|
||||
const sub = useMemo(() => {
|
||||
const b = new RequestBuilder(`followers`);
|
||||
if (pubkey) {
|
||||
@ -17,9 +20,7 @@ export default function useFollowersFeed(pubkey?: HexKey) {
|
||||
const contactLists = followersFeed?.filter(
|
||||
a => a.kind === EventKind.ContactList && a.tags.some(b => b[0] === "p" && b[1] === pubkey),
|
||||
);
|
||||
return [...new Set(contactLists?.map(a => a.pubkey))].sort((a, b) => {
|
||||
return socialGraphInstance.getFollowDistance(a) - socialGraphInstance.getFollowDistance(b);
|
||||
});
|
||||
return wot.sortEvents(contactLists);
|
||||
}, [followersFeed, pubkey]);
|
||||
|
||||
return followers;
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { unixNowMs } from "@snort/shared";
|
||||
import { EventKind, RequestBuilder } from "@snort/system";
|
||||
import { useRequestBuilder } from "@snort/system-react";
|
||||
import { useEffect, useMemo } from "react";
|
||||
@ -5,6 +6,7 @@ import { useEffect, useMemo } from "react";
|
||||
import useEventPublisher from "@/Hooks/useEventPublisher";
|
||||
import useLogin from "@/Hooks/useLogin";
|
||||
import usePreferences from "@/Hooks/usePreferences";
|
||||
import { System } from "@/system";
|
||||
import { bech32ToHex, unwrap } from "@/Utils";
|
||||
import { SnortPubKey } from "@/Utils/Const";
|
||||
import { addSubscription } from "@/Utils/Login";
|
||||
@ -23,6 +25,17 @@ export default function useLoginFeed() {
|
||||
system.checkSigs = checkSigs;
|
||||
}, [system, checkSigs]);
|
||||
|
||||
useEffect(() => {
|
||||
if (pubKey) {
|
||||
const start = unixNowMs();
|
||||
system.config.socialGraphInstance.setRoot(pubKey);
|
||||
console.debug(
|
||||
`Social graph loaded in ${(unixNowMs() - start).toFixed(2)}ms`,
|
||||
System.config.socialGraphInstance.size(),
|
||||
);
|
||||
}
|
||||
}, [pubKey]);
|
||||
|
||||
useEffect(() => {
|
||||
login.state.init(publisher?.signer, system).catch(console.error);
|
||||
}, [login, publisher, system]);
|
||||
|
@ -1,10 +1,11 @@
|
||||
import { socialGraphInstance } from "@snort/system";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
|
||||
import fuzzySearch from "@/Db/FuzzySearch";
|
||||
import useTimelineFeed, { TimelineFeedOptions, TimelineSubject } from "@/Feed/TimelineFeed";
|
||||
import { debounce } from "@/Utils";
|
||||
|
||||
import useWoT from "./useWoT";
|
||||
|
||||
const options: TimelineFeedOptions = { method: "LIMIT_UNTIL" };
|
||||
|
||||
export default function useProfileSearch(search: string) {
|
||||
@ -34,6 +35,7 @@ export default function useProfileSearch(search: string) {
|
||||
}
|
||||
|
||||
export function userSearch(search: string) {
|
||||
const wot = useWoT();
|
||||
const searchString = search.trim();
|
||||
const fuseResults = fuzzySearch.search(searchString);
|
||||
|
||||
@ -44,7 +46,7 @@ export function userSearch(search: string) {
|
||||
.map(result => {
|
||||
const fuseScore = result.score === undefined ? 1 : result.score;
|
||||
|
||||
const followDistance = wotScore(result.item.pubkey) / followDistanceNormalizationFactor;
|
||||
const followDistance = wot.followDistance(result.item.pubkey) / followDistanceNormalizationFactor;
|
||||
|
||||
const startsWithSearchString = [result.item.name, result.item.display_name, result.item.nip05].some(
|
||||
field => field && field.toLowerCase?.().startsWith(searchString.toLowerCase()),
|
||||
@ -72,11 +74,3 @@ export function userSearch(search: string) {
|
||||
|
||||
return combinedResults.map(r => r.item);
|
||||
}
|
||||
|
||||
export function wotScore(pubkey: string) {
|
||||
return socialGraphInstance.getFollowDistance(pubkey);
|
||||
}
|
||||
|
||||
export function sortByWoT(pubkeys: Array<string>) {
|
||||
return pubkeys.sort((a, b) => (wotScore(a) > wotScore(b) ? 1 : -1));
|
||||
}
|
||||
|
@ -1,15 +1,28 @@
|
||||
import { socialGraphInstance, TaggedNostrEvent } from "@snort/system";
|
||||
import { useMemo } from "react";
|
||||
import { TaggedNostrEvent } from "@snort/system";
|
||||
import { SnortContext } from "@snort/system-react";
|
||||
import { useContext, useMemo } from "react";
|
||||
|
||||
export default function useWoT() {
|
||||
const system = useContext(SnortContext);
|
||||
return useMemo(
|
||||
() => ({
|
||||
sortEvents: (events: Array<TaggedNostrEvent>) =>
|
||||
events.sort(
|
||||
(a, b) => socialGraphInstance.getFollowDistance(a.pubkey) - socialGraphInstance.getFollowDistance(b.pubkey),
|
||||
(a, b) =>
|
||||
system.config.socialGraphInstance.getFollowDistance(a.pubkey) -
|
||||
system.config.socialGraphInstance.getFollowDistance(b.pubkey),
|
||||
),
|
||||
followDistance: (pk: string) => socialGraphInstance.getFollowDistance(pk),
|
||||
sortPubkeys: (events: Array<string>) =>
|
||||
events.sort(
|
||||
(a, b) =>
|
||||
system.config.socialGraphInstance.getFollowDistance(a) -
|
||||
system.config.socialGraphInstance.getFollowDistance(b),
|
||||
),
|
||||
followDistance: (pk: string) => system.config.socialGraphInstance.getFollowDistance(pk),
|
||||
followedByCount: (pk: string) => system.config.socialGraphInstance.followedByFriendsCount(pk),
|
||||
followedBy: (pk: string) => system.config.socialGraphInstance.followedByFriends(pk),
|
||||
instance: system.config.socialGraphInstance,
|
||||
}),
|
||||
[socialGraphInstance.root],
|
||||
[system.config.socialGraphInstance],
|
||||
);
|
||||
}
|
||||
|
@ -44,11 +44,6 @@ const MENU_ITEMS = [
|
||||
icon: "deck",
|
||||
link: "/deck",
|
||||
},
|
||||
{
|
||||
label: <FormattedMessage defaultMessage="Social Graph" />,
|
||||
icon: "graph",
|
||||
link: "/graph",
|
||||
},
|
||||
{
|
||||
label: <FormattedMessage defaultMessage="About" />,
|
||||
icon: "info",
|
||||
|
@ -3,7 +3,6 @@ import classNames from "classnames";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import AsyncButton from "@/Components/Button/AsyncButton";
|
||||
import Icon from "@/Components/Icons/Icon";
|
||||
import ProfileImage from "@/Components/User/ProfileImage";
|
||||
import ProfilePreview from "@/Components/User/ProfilePreview";
|
||||
@ -79,11 +78,6 @@ export default function ProfileMenu({ className }: { className?: string }) {
|
||||
/>
|
||||
</MenuItem>
|
||||
))}
|
||||
<MenuItem>
|
||||
<AsyncButton className="!bg-gray-light !text-white">
|
||||
<FormattedMessage defaultMessage="Add Account" />
|
||||
</AsyncButton>
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</div>
|
||||
);
|
||||
|
@ -1,13 +0,0 @@
|
||||
import { NodeObject } from "react-force-graph-3d";
|
||||
|
||||
import { proxyImg } from "@/Hooks/useImgProxy";
|
||||
import { GraphNode } from "@/Pages/NetworkGraph/types";
|
||||
import { defaultAvatar } from "@/Utils";
|
||||
import { LoginStore } from "@/Utils/Login";
|
||||
|
||||
export const avatar = (node: NodeObject<NodeObject<GraphNode>>) => {
|
||||
const login = LoginStore.snapshot();
|
||||
return node.profile?.picture
|
||||
? proxyImg(node.profile?.picture, login.state.appdata?.preferences.imgProxyConfig)
|
||||
: defaultAvatar(node.address);
|
||||
};
|
@ -1,248 +0,0 @@
|
||||
import { socialGraphInstance, STR, UID } from "@snort/system";
|
||||
import { SnortContext } from "@snort/system-react";
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import { NodeObject } from "react-force-graph-3d";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
|
||||
import Icon from "@/Components/Icons/Icon";
|
||||
import { avatar } from "@/Pages/NetworkGraph/Avatar";
|
||||
import { Direction, GraphConfig, GraphData, GraphLink, GraphNode, NODE_LIMIT } from "@/Pages/NetworkGraph/types";
|
||||
|
||||
const NetworkGraph = () => {
|
||||
const [graphData, setGraphData] = useState(null as GraphData | null);
|
||||
const [graphConfig, setGraphConfig] = useState({
|
||||
direction: Direction.OUTBOUND,
|
||||
renderLimit: NODE_LIMIT,
|
||||
showDistance: 2,
|
||||
});
|
||||
const [open, setOpen] = useState(false);
|
||||
const system = useContext(SnortContext);
|
||||
// const [showDistance, setShowDistance] = useState(2);
|
||||
// const [direction, setDirection] = useState(Direction.OUTBOUND);
|
||||
// const [renderLimit, setRenderLimit] = useState(NODE_LIMIT);
|
||||
|
||||
const [ForceGraph3D, setForceGraph3D] = useState(null);
|
||||
const [THREE, setTHREE] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
// Dynamically import the modules
|
||||
import("react-force-graph-3d").then(module => {
|
||||
setForceGraph3D(module.default);
|
||||
});
|
||||
import("three").then(module => {
|
||||
setTHREE(module);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleCloseGraph = () => {
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
const handleKeyDown = (event: { key: string }) => {
|
||||
if (event.key === "Escape") {
|
||||
handleCloseGraph();
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
}
|
||||
|
||||
// Cleanup function to remove the event listener
|
||||
return () => {
|
||||
window.removeEventListener("keydown", handleKeyDown);
|
||||
};
|
||||
}, [open]);
|
||||
|
||||
const updateConfig = async (changes: Partial<GraphConfig>) => {
|
||||
setGraphConfig(old => {
|
||||
const newConfig = Object.assign({}, old, changes);
|
||||
updateGraph(newConfig).then(graph => setGraphData(graph));
|
||||
return newConfig;
|
||||
});
|
||||
};
|
||||
|
||||
const toggleConnections = () => {
|
||||
if (graphConfig.direction === Direction.OUTBOUND) {
|
||||
updateConfig({ direction: Direction.BOTH });
|
||||
} else {
|
||||
updateConfig({ direction: Direction.OUTBOUND });
|
||||
}
|
||||
};
|
||||
|
||||
const updateGraph = async (newConfig?: GraphConfig) => {
|
||||
const { direction, renderLimit, showDistance } = newConfig ?? graphConfig;
|
||||
const nodes = new Map<number, GraphNode>();
|
||||
const links: GraphLink[] = [];
|
||||
const nodesVisited = new Set<UID>();
|
||||
const userCountByDistance = Array.from(
|
||||
{ length: 6 },
|
||||
(_, i) => socialGraphInstance.usersByFollowDistance.get(i)?.size || 0,
|
||||
);
|
||||
|
||||
// Go through all the nodes
|
||||
for (let distance = 0; distance <= showDistance; ++distance) {
|
||||
const users = socialGraphInstance.usersByFollowDistance.get(distance);
|
||||
if (!users) break;
|
||||
for (const UID of users) {
|
||||
if (renderLimit && nodes.size >= renderLimit) break; // Temporary hack
|
||||
const inboundCount = socialGraphInstance.followersByUser.get(UID)?.size || 0;
|
||||
const outboundCount = socialGraphInstance.followedByUser.get(UID)?.size || 0;
|
||||
const pubkey = STR(UID);
|
||||
const node = {
|
||||
id: UID,
|
||||
address: pubkey,
|
||||
profile: system.profileLoader.cache.getFromCache(pubkey),
|
||||
distance,
|
||||
inboundCount,
|
||||
outboundCount,
|
||||
visible: true, // Setting to false only hides the rendered element, does not prevent calculations
|
||||
// curvature: 0.6,
|
||||
// Node size is based on the follower count
|
||||
val: Math.log10(inboundCount) + 1, // 1 followers -> 1, 10 followers -> 2, 100 followers -> 3, etc.,
|
||||
} as GraphNode;
|
||||
// A visibility boost for the origin user:
|
||||
if (node.distance === 0) {
|
||||
node.val = 10; // they're always larger than life
|
||||
node.color = "#603285";
|
||||
}
|
||||
nodes.set(UID, node);
|
||||
}
|
||||
}
|
||||
|
||||
// Add links
|
||||
for (const node of nodes.values()) {
|
||||
if (direction === Direction.OUTBOUND || direction === Direction.BOTH) {
|
||||
for (const followedID of socialGraphInstance.followedByUser.get(node.id) ?? []) {
|
||||
if (!nodes.has(followedID)) continue; // Skip links to nodes that we're not rendering
|
||||
if (nodesVisited.has(followedID)) continue;
|
||||
links.push({
|
||||
source: node.id,
|
||||
target: followedID,
|
||||
distance: node.distance,
|
||||
});
|
||||
}
|
||||
}
|
||||
// TODO: Fix filtering
|
||||
/* if (direction === Direction.INBOUND || direction === Direction.BOTH) {
|
||||
for (const followerID of socialGraphInstance.followersByUser.get(node.id) ?? []) {
|
||||
if (nodesVisited.has(followerID)) continue;
|
||||
const follower = nodes.get(followerID);
|
||||
if (!follower) continue; // Skip links to nodes that we're not rendering
|
||||
links.push({
|
||||
source: followerID,
|
||||
target: node.id,
|
||||
distance: follower.distance,
|
||||
});
|
||||
}
|
||||
}*/
|
||||
nodesVisited.add(node.id);
|
||||
}
|
||||
|
||||
// Squash cases, where there are a lot of nodes
|
||||
|
||||
const graph: GraphData = {
|
||||
nodes: [...nodes.values()],
|
||||
links,
|
||||
meta: {
|
||||
nodes,
|
||||
userCountByDistance,
|
||||
},
|
||||
};
|
||||
|
||||
// console.log('!!', graph);
|
||||
// for (const l of links) {
|
||||
// if (!nodes.has(l.source)) {
|
||||
// console.log('source missing:', l.source);
|
||||
// }
|
||||
// if (!nodes.has(l.target)) {
|
||||
// console.log('target missing:', l.target);
|
||||
// }
|
||||
// }
|
||||
|
||||
return graph;
|
||||
};
|
||||
|
||||
const refreshData = async () => {
|
||||
updateGraph().then(setGraphData);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
refreshData();
|
||||
}, []);
|
||||
|
||||
if (!ForceGraph3D || !THREE) return null;
|
||||
|
||||
return (
|
||||
<div>
|
||||
{!open && (
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={() => {
|
||||
setOpen(true);
|
||||
refreshData();
|
||||
}}>
|
||||
<FormattedMessage defaultMessage="Show graph" />
|
||||
</button>
|
||||
)}
|
||||
{open && graphData && (
|
||||
<div className="fixed top-0 left-0 right-0 bottom-0 z-20">
|
||||
<button className="absolute top-6 right-6 z-30 btn hover:bg-gray-900" onClick={handleCloseGraph}>
|
||||
<Icon name="x" size={24} />
|
||||
</button>
|
||||
<div className="absolute top-6 right-0 left-0 z-20 flex flex-col content-center justify-center text-center">
|
||||
<div className="text-center pb-2">Degrees of separation</div>
|
||||
<div className="flex flex-row justify-center space-x-4">
|
||||
{graphData.meta?.userCountByDistance?.map((value, i) => {
|
||||
if (i === 0 || value <= 0) return null;
|
||||
const isSelected = graphConfig.showDistance === i;
|
||||
return (
|
||||
<button
|
||||
key={i}
|
||||
className={`btn bg-gray-900 py-4 h-auto flex-col ${
|
||||
isSelected ? "bg-gray-600 hover:bg-gray-600" : "hover:bg-gray-800"
|
||||
}`}
|
||||
onClick={() => isSelected || updateConfig({ showDistance: i })}>
|
||||
<div className="text-lg block leading-none">{i}</div>
|
||||
<div className="text-xs">({value})</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<ForceGraph3D
|
||||
graphData={graphData}
|
||||
nodeThreeObject={(node: NodeObject<NodeObject<GraphNode>>) => {
|
||||
const imgTexture = new THREE.TextureLoader().load(avatar(node));
|
||||
imgTexture.colorSpace = THREE.SRGBColorSpace;
|
||||
const material = new THREE.SpriteMaterial({ map: imgTexture });
|
||||
const sprite = new THREE.Sprite(material);
|
||||
sprite.scale.set(12, 12, 1);
|
||||
|
||||
return sprite;
|
||||
}}
|
||||
nodeLabel={node => `${node.profile?.name || node.address}`}
|
||||
nodeAutoColorBy="distance"
|
||||
linkAutoColorBy="distance"
|
||||
linkDirectionalParticles={0}
|
||||
nodeVisibility="visible"
|
||||
numDimensions={3}
|
||||
linkDirectionalArrowLength={0}
|
||||
nodeOpacity={0.9}
|
||||
/>
|
||||
<div className="absolute bottom-6 right-6">
|
||||
<button className="text-lg" onClick={() => toggleConnections()}>
|
||||
Showing: {graphConfig.direction === Direction.OUTBOUND ? "Outbound" : "All"} connections
|
||||
</button>
|
||||
</div>
|
||||
<div className="absolute bottom-6 left-6">
|
||||
<span className="text-lg">Render limit: {graphConfig.renderLimit} nodes</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default NetworkGraph;
|
@ -1,45 +0,0 @@
|
||||
import { CachedMetadata, UID } from "@snort/system";
|
||||
|
||||
export interface GraphNode {
|
||||
id: UID;
|
||||
profile?: CachedMetadata;
|
||||
distance: number;
|
||||
val: number;
|
||||
inboundCount: number;
|
||||
outboundCount: number;
|
||||
color?: string;
|
||||
visible: boolean;
|
||||
// curvature?: number;
|
||||
}
|
||||
|
||||
export interface GraphLink {
|
||||
source: UID;
|
||||
target: UID;
|
||||
distance: number;
|
||||
}
|
||||
|
||||
interface GraphMetadata {
|
||||
// usersByFollowDistance?: Map<number, Set<UID>>;
|
||||
userCountByDistance: number[];
|
||||
nodes?: Map<number, GraphNode>;
|
||||
}
|
||||
|
||||
export interface GraphData {
|
||||
nodes: GraphNode[];
|
||||
links: GraphLink[];
|
||||
meta?: GraphMetadata;
|
||||
}
|
||||
|
||||
export enum Direction {
|
||||
INBOUND,
|
||||
OUTBOUND,
|
||||
BOTH,
|
||||
}
|
||||
|
||||
export interface GraphConfig {
|
||||
direction: Direction;
|
||||
renderLimit: number | null;
|
||||
showDistance: number;
|
||||
}
|
||||
|
||||
export const NODE_LIMIT = 500;
|
@ -9,7 +9,7 @@ import { useNavigate } from "react-router-dom";
|
||||
import NoteTime from "@/Components/Event/Note/NoteTime";
|
||||
import Icon from "@/Components/Icons/Icon";
|
||||
import ProfileImage from "@/Components/User/ProfileImage";
|
||||
import { sortByWoT } from "@/Hooks/useProfileSearch";
|
||||
import useWoT from "@/Hooks/useWoT";
|
||||
import { dedupe, getDisplayName } from "@/Utils";
|
||||
import { formatShort } from "@/Utils/Number";
|
||||
|
||||
@ -25,6 +25,7 @@ export function NotificationGroup({
|
||||
}) {
|
||||
const { ref, inView } = useInView({ triggerOnce: true });
|
||||
const { formatMessage } = useIntl();
|
||||
const wot = useWoT();
|
||||
const kind = evs[0].kind;
|
||||
const navigate = useNavigate();
|
||||
|
||||
@ -127,7 +128,8 @@ export function NotificationGroup({
|
||||
<div className="flex flex-col w-max g12">
|
||||
<div className="flex flex-row w-max overflow-hidden justify-between items-center">
|
||||
<div className="flex flex-row">
|
||||
{sortByWoT(pubkeys.filter(a => a !== "anon"))
|
||||
{wot
|
||||
.sortPubkeys(pubkeys.filter(a => a !== "anon"))
|
||||
.slice(0, 12)
|
||||
.map(v => (
|
||||
<ProfileImage
|
||||
|
@ -9,7 +9,6 @@ import {
|
||||
KeyStorage,
|
||||
NotEncrypted,
|
||||
RelaySettings,
|
||||
socialGraphInstance,
|
||||
UserState,
|
||||
UserStateObject,
|
||||
} from "@snort/system";
|
||||
@ -84,10 +83,6 @@ export class MultiAccountStore extends ExternalStore<LoginSession> {
|
||||
if (!this.#activeAccount) {
|
||||
this.#activeAccount = this.#accounts.keys().next().value;
|
||||
}
|
||||
if (this.#activeAccount) {
|
||||
const pubKey = this.#accounts.get(this.#activeAccount)?.publicKey;
|
||||
socialGraphInstance.setRoot(pubKey || "");
|
||||
}
|
||||
for (const [, v] of this.#accounts) {
|
||||
// reset readonly on load
|
||||
if (v.type === LoginSessionType.PrivateKey && v.readonly) {
|
||||
@ -146,8 +141,6 @@ export class MultiAccountStore extends ExternalStore<LoginSession> {
|
||||
switchAccount(id: string) {
|
||||
if (this.#accounts.has(id)) {
|
||||
this.#activeAccount = id;
|
||||
const pubKey = this.#accounts.get(id)?.publicKey || "";
|
||||
socialGraphInstance.setRoot(pubKey);
|
||||
this.#save();
|
||||
}
|
||||
}
|
||||
@ -172,7 +165,6 @@ export class MultiAccountStore extends ExternalStore<LoginSession> {
|
||||
if (this.#accounts.has(key)) {
|
||||
throw new Error("Already logged in with this pubkey");
|
||||
}
|
||||
socialGraphInstance.setRoot(key);
|
||||
const initRelays = this.decideInitRelays(relays);
|
||||
const newSession = {
|
||||
...LoggedOut,
|
||||
@ -224,7 +216,6 @@ export class MultiAccountStore extends ExternalStore<LoginSession> {
|
||||
if (this.#accounts.has(pubKey)) {
|
||||
throw new Error("Already logged in with this pubkey");
|
||||
}
|
||||
socialGraphInstance.setRoot(pubKey);
|
||||
const initRelays = this.decideInitRelays(relays);
|
||||
const newSession = {
|
||||
...LoggedOut,
|
||||
|
@ -3,7 +3,6 @@ import "@szhsin/react-menu/dist/index.css";
|
||||
import "@/assets/fonts/inter.css";
|
||||
|
||||
import { unixNow, unixNowMs } from "@snort/shared";
|
||||
import { socialGraphInstance } from "@snort/system";
|
||||
import { SnortContext } from "@snort/system-react";
|
||||
import { StrictMode } from "react";
|
||||
import * as ReactDOM from "react-dom/client";
|
||||
@ -24,7 +23,6 @@ import HelpPage from "@/Pages/HelpPage";
|
||||
import Layout from "@/Pages/Layout";
|
||||
import { ListFeedPage } from "@/Pages/ListFeedPage";
|
||||
import MessagesPage from "@/Pages/Messages/MessagesPage";
|
||||
import NetworkGraph from "@/Pages/NetworkGraph/NetworkGraph";
|
||||
import NostrAddressPage from "@/Pages/NostrAddressPage";
|
||||
import NostrLinkHandler from "@/Pages/NostrLinkHandler";
|
||||
import NotificationsPage from "@/Pages/Notifications/Notifications";
|
||||
@ -63,11 +61,10 @@ async function initSite() {
|
||||
preload(login.state.follows).then(async () => {
|
||||
queueMicrotask(async () => {
|
||||
const start = unixNowMs();
|
||||
await System.PreloadSocialGraph(login.state.follows);
|
||||
await System.PreloadSocialGraph(login.state.follows, login.publicKey);
|
||||
console.debug(
|
||||
`Social graph loaded in ${(unixNowMs() - start).toFixed(2)}ms, followDistances=${
|
||||
socialGraphInstance.followDistanceByUser.size
|
||||
}`,
|
||||
`Social graph loaded in ${(unixNowMs() - start).toFixed(2)}ms`,
|
||||
System.config.socialGraphInstance.size(),
|
||||
);
|
||||
});
|
||||
|
||||
@ -137,10 +134,6 @@ const mainRoutes = [
|
||||
path: "/about",
|
||||
element: <AboutPage />,
|
||||
},
|
||||
{
|
||||
path: "/graph",
|
||||
element: <NetworkGraph />,
|
||||
},
|
||||
{
|
||||
path: "/wallet",
|
||||
element: (
|
||||
|
@ -1776,9 +1776,6 @@
|
||||
"hYOE+U": {
|
||||
"defaultMessage": "Invite"
|
||||
},
|
||||
"ha8JKG": {
|
||||
"defaultMessage": "Show graph"
|
||||
},
|
||||
"hicxcO": {
|
||||
"defaultMessage": "Show replies"
|
||||
},
|
||||
|
@ -589,7 +589,6 @@
|
||||
"hRTfTR": "PRO",
|
||||
"hY4lzx": "Supports",
|
||||
"hYOE+U": "Invite",
|
||||
"ha8JKG": "Show graph",
|
||||
"hicxcO": "Show replies",
|
||||
"hmZ3Bz": "Media",
|
||||
"hniz8Z": "here",
|
||||
|
Reference in New Issue
Block a user