Skip to content

Commit

Permalink
Merge branch 'master' into production
Browse files Browse the repository at this point in the history
  • Loading branch information
mmalmi committed Oct 12, 2023
2 parents e8a53f6 + af62fa2 commit 7a67876
Show file tree
Hide file tree
Showing 7 changed files with 1,728 additions and 997 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"preact": "^10.17.1",
"preact-async-route": "^2.2.1",
"preact-router": "^4.1.2",
"react-force-graph-3d": "^1.23.1",
"react-helmet": "^6.1.0",
"react-string-replace": "^1.1.1"
},
Expand Down
6 changes: 3 additions & 3 deletions src/js/components/header/NotificationsButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,13 @@ export default function NotificationsButton() {
className={`relative inline-block rounded-full ${isMyProfile ? 'hidden md:flex' : ''}`}
>
<Show when={activeRoute === '/notifications'}>
<HeartIconFull width={28} />
<HeartIconFull class={unseenNotificationCount ? 'mr-3' : ''} width={28} />
</Show>
<Show when={activeRoute !== '/notifications'}>
<HeartIcon width={28} />
<HeartIcon class={unseenNotificationCount ? 'mr-3' : ''} width={28} />
</Show>
<Show when={unseenNotificationCount}>
<span className="absolute top-0 right-0 transform translate-x-1/2 -translate-y-1/2 bg-iris-purple text-white text-sm rounded-full h-5 w-5 flex items-center justify-center">
<span className="absolute top-1 right-3 transform translate-x-1/2 -translate-y-1/2 bg-iris-purple text-white text-sm rounded-full h-5 w-5 flex items-center justify-center">
{unseenNotificationCount > 99 ? '' : unseenNotificationCount}
</span>
</Show>
Expand Down
4 changes: 2 additions & 2 deletions src/js/components/modal/Zap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -387,9 +387,9 @@ export default function SendSats(props: ZapProps) {

return (
<Modal showContainer={true} centerVertically={true} onClose={onClose}>
<div className="bg-black rounded-lg p-8 w-[400px] relative">
<div className="bg-black rounded-lg p-8 relative">
<div className="lnurl-tip" onClick={(e) => e.stopPropagation()}>
<div className="absolute top-2.5 right-2.5 cursor-pointer">
<div className="absolute top-2.5 right-2.5 cursor-pointer" onClick={onClose}>
<XMarkIcon width={20} height={20} />
</div>
<div className="lnurl-header">
Expand Down
2 changes: 1 addition & 1 deletion src/js/nostr/SocialNetwork.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ export default {
if (this.followedByUser.get(myId)?.has(follower)) {
if (!PubSub.subscribedAuthors.has(STR(followedUser))) {
setTimeout(() => {
PubSub.subscribe({ authors: [STR(followedUser)] }, undefined, true);
PubSub.subscribe({ authors: [STR(followedUser)], kinds: [0, 3] }, undefined, true);
}, 0);
}
}
Expand Down
229 changes: 229 additions & 0 deletions src/js/views/NetworkGraph.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
import ForceGraph3D from 'react-force-graph-3d';
import { useEffect, useState } from 'preact/hooks';

import SocialNetwork from '../nostr/SocialNetwork';
import { STR, UID } from '../utils/UniqueIds';

interface GraphNode {
id: UID;
profile?: any;
distance: number;
val: number;
inboundCount: number;
outboundCount: number;
color?: string;
visible: boolean;
// curvature?: number;
}

interface GraphLink {
source: UID;
target: UID;
distance: number;
}

interface GraphMetadata {
// usersByFollowDistance?: Map<number, Set<UID>>;
userCountByDistance: number[];
nodes?: Map<number, GraphNode>;
}

interface GraphData {
nodes: GraphNode[];
links: GraphLink[];
meta?: GraphMetadata;
}

enum Direction {
INBOUND,
OUTBOUND,
BOTH,
}

const NODE_LIMIT = 500;

interface GraphConfig {
direction: Direction;
renderLimit: number | null;
showDistance: number;
}

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 [showDistance, setShowDistance] = useState(2);
// const [direction, setDirection] = useState(Direction.OUTBOUND);
// const [renderLimit, setRenderLimit] = useState(NODE_LIMIT);

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) => SocialNetwork.usersByFollowDistance.get(i)?.size || 0,
);

// Go through all the nodes
for (let distance = 0; distance <= showDistance; ++distance) {
const users = SocialNetwork.usersByFollowDistance.get(distance);
if (!users) break;
for (const UID of users) {
if (renderLimit && nodes.size >= renderLimit) break; // Temporary hack
const inboundCount = SocialNetwork.followersByUser.get(UID)?.size || 0;
const outboundCount = SocialNetwork.followedByUser.get(UID)?.size || 0;
const node = {
id: UID,
address: STR(UID),
profile: SocialNetwork.profiles.get(UID),
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 SocialNetwork.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) {

Check failure on line 132 in src/js/views/NetworkGraph.tsx

View workflow job for this annotation

GitHub Actions / lint

Insert `······`
for (const followerID of SocialNetwork.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();
}, []);

return (
<div>
{!open && (
<button class="btn btn-primary" onClick={() => { setOpen(true); refreshData(); }}>

Check failure on line 182 in src/js/views/NetworkGraph.tsx

View workflow job for this annotation

GitHub Actions / lint

Replace `·class="btn·btn-primary"·onClick={()·=>·{·setOpen(true);·refreshData();·}}` with `⏎··········class="btn·btn-primary"⏎··········onClick={()·=>·{⏎············setOpen(true);⏎············refreshData();⏎··········}}⏎········`
Show graph
</button>
)}
{open && graphData && (
<div className="fixed top-0 left-0 right-0 bottom-0 z-20">
<button class="absolute top-6 right-6 z-30 btn hover:bg-gray-900" onClick={() => setOpen(false)}>X</button>

Check failure on line 188 in src/js/views/NetworkGraph.tsx

View workflow job for this annotation

GitHub Actions / lint

Replace `·class="absolute·top-6·right-6·z-30·btn·hover:bg-gray-900"·onClick={()·=>·setOpen(false)}>X` with `⏎············class="absolute·top-6·right-6·z-30·btn·hover:bg-gray-900"⏎············onClick={()·=>·setOpen(false)}⏎··········>⏎············X⏎··········`
<div class="absolute top-6 right-0 left-0 z-20 flex flex-col content-center justify-center text-center">
<div class="text-center pb-2">Degrees of separation</div>
<div class="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 class={`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 }) }>

Check failure on line 196 in src/js/views/NetworkGraph.tsx

View workflow job for this annotation

GitHub Actions / lint

Replace `·class={`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·})·}` with `⏎····················class={`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 class="text-lg block leading-none">{i}</div>
<div class="text-xs">({value})</div>
</button>
);
})}
</div>
</div>
<ForceGraph3D
graphData={graphData}
nodeLabel={(node) => `${node.profile?.name || node.address}`}
nodeAutoColorBy="distance"
linkAutoColorBy="distance"
linkDirectionalParticles={1}
nodeVisibility="visible"
numDimensions={3}
linkDirectionalArrowLength={0}
nodeOpacity={0.9}
/>
<div class="absolute bottom-6 right-6">
<button class="text-lg" onClick={() => toggleConnections()}>
Showing: { graphConfig.direction === Direction.OUTBOUND ? 'Outbound' : 'All' } connections

Check failure on line 217 in src/js/views/NetworkGraph.tsx

View workflow job for this annotation

GitHub Actions / lint

Replace `·graphConfig.direction·===·Direction.OUTBOUND·?·'Outbound'·:·'All'·}` with `graphConfig.direction·===·Direction.OUTBOUND·?·'Outbound'·:·'All'}{'·'}⏎·············`
</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;
2 changes: 2 additions & 0 deletions src/js/views/settings/SocialNetwork.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import Key from '../../nostr/Key';
import SocialNetwork from '../../nostr/SocialNetwork';
import localState from '../../state/LocalState.ts';
import { translate as t } from '../../translations/Translation.mjs';
import NetworkGraph from "../NetworkGraph";

Check failure on line 10 in src/js/views/settings/SocialNetwork.tsx

View workflow job for this annotation

GitHub Actions / lint

Replace `"../NetworkGraph"` with `'../NetworkGraph'`

const SocialNetworkSettings = () => {
const [blockedUsers, setBlockedUsers] = useState<string[]>([]);
Expand Down Expand Up @@ -64,6 +65,7 @@ const SocialNetworkSettings = () => {
{distance[0] || t('unknown')}: {distance[1].size} users
</div>
))}
<NetworkGraph />
<p>Filter incoming events by follow distance:</p>
<select
className="select"
Expand Down
Loading

0 comments on commit 7a67876

Please sign in to comment.