Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@ import { createClient } from "./api";
import { GetAccountMetadataDocument } from "./vendor/intuition-graphql/dist/index.mjs";
import RealityTunnel from "./RealityTunnel";
import EndpointSelector from "./EndpointSelector";
import TrustCirclePanel from "./TrustCirclePanel";

function App() {
const [endpoint, setEndpoint] = useState("base");
const [userFilterAddress, setUserFilterAddress] = useState(null);
const [trustGraphData, setTrustGraphData] = useState(null);
const { address, isConnected } = useAccount();
const [accountLabel, setAccountLabel] = useState("");

Expand Down Expand Up @@ -56,8 +58,8 @@ function App() {
connectedLabel={accountLabel}
/>
<div className="header-right">
<span className="env-badge" title="Using Intuition Testnet API">
<span className="env-dot" /> Intuition Testnet
<span className="env-badge" title="Using Intuition Mainnet API">
<span className="env-dot" /> Intuition Mainnet
</span>
<ConnectButton.Custom>
{({ account, openConnectModal, openAccountModal, mounted }) => {
Expand Down Expand Up @@ -85,6 +87,13 @@ function App() {
<GraphVisualization
endpoint={endpoint}
userFilterAddress={userFilterAddress}
trustGraphData={trustGraphData}
/>
<TrustCirclePanel
endpoint={endpoint}
connectedAddress={address}
connectedLabel={accountLabel}
onGraphData={setTrustGraphData}
/>
</main>
</div>
Expand Down
41 changes: 33 additions & 8 deletions src/GraphVisualization.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import GraphVR from "./GraphVR";
import NodeDetailsSidebar from "./NodeDetailsSidebar";
import LoadingAnimation from "./LoadingAnimation";

const GraphVisualization = ({ endpoint, userFilterAddress }) => {
const GraphVisualization = ({ endpoint, userFilterAddress, trustGraphData }) => {
const [graphData, setGraphData] = useState({ nodes: [], links: [] });
const [initialGraphData, setInitialGraphData] = useState(null);
const [isInitialLoad, setIsInitialLoad] = useState(true);
Expand Down Expand Up @@ -338,9 +338,31 @@ const GraphVisualization = ({ endpoint, userFilterAddress }) => {
}
}, [shouldSearch, applyFilters]);

// When the Trust Circle reality tunnel is active, it supplies its own
// weighted graph and takes over rendering without disturbing the normal
// triples/search state above (so turning it off just falls back to that).
const displayGraphData = trustGraphData || graphData;

return (
<div>
{(isLoading || isSearching) && <LoadingAnimation />}
{trustGraphData && (
<div
style={{
position: "absolute",
top: "10px",
left: "165px",
zIndex: 50,
background: "rgba(157, 78, 221, 0.85)",
color: "#fff",
padding: "6px 10px",
borderRadius: "999px",
fontSize: "12px",
}}
>
Trust Circle view
</div>
)}
<button
className="navigation-button"
onClick={resetGraph}
Expand Down Expand Up @@ -475,15 +497,16 @@ const GraphVisualization = ({ endpoint, userFilterAddress }) => {
{viewMode === "2D" && (
<ForceGraph2D
ref={(el) => (fgRef.current = el)}
graphData={graphData}
graphData={displayGraphData}
nodeCanvasObject={(node, ctx, globalScale) => {
const label = node.label || "";
const fontSize = 12 / globalScale;
const sizeScale = node.val || 1;
const fontSize = (12 * sizeScale) / globalScale;
ctx.font = `${fontSize}px Sans-Serif`;

const textWidth = ctx.measureText(label).width;
const padding = 10 / globalScale;
const radius = 5 / globalScale;
const padding = (10 * sizeScale) / globalScale;
const radius = (5 * sizeScale) / globalScale;

ctx.fillStyle = node.color + "CC";
const x = node.x - textWidth / 2 - padding;
Expand Down Expand Up @@ -536,6 +559,7 @@ const GraphVisualization = ({ endpoint, userFilterAddress }) => {
);
}}
linkColor={() => "#666"}
linkWidth={(link) => link.width || 1}
linkDirectionalParticles={1}
linkDirectionalParticleSpeed={0.02}
linkDirectionalParticleColor={() => "#fff"}
Expand All @@ -548,11 +572,12 @@ const GraphVisualization = ({ endpoint, userFilterAddress }) => {
{viewMode === "3D" && (
<ForceGraph3D
ref={(el) => (fgRef.current = el)}
graphData={graphData}
graphData={displayGraphData}
controlType="fly"
nodeLabel="label"
onNodeClick={handleNodeClick}
linkColor={() => "#666"}
linkWidth={(link) => link.width || 1}
linkDirectionalParticles={2}
linkDirectionalParticleSpeed={0.005}
nodeAutoColorBy="type"
Expand All @@ -562,7 +587,7 @@ const GraphVisualization = ({ endpoint, userFilterAddress }) => {
sprite.backgroundColor = node.color + "CC";
sprite.padding = 1;
sprite.color = "#fff";
sprite.textHeight = 2;
sprite.textHeight = 2 * (node.val || 1);
return sprite;
}}
onEngineStop={handleEngineStop}
Expand All @@ -571,7 +596,7 @@ const GraphVisualization = ({ endpoint, userFilterAddress }) => {

{viewMode === "VR" && (
<GraphVR
graphData={graphData}
graphData={displayGraphData}
onNodeClick={handleNodeClick}
onBack={goBack}
onForward={goForward}
Expand Down
206 changes: 206 additions & 0 deletions src/TrustCirclePanel.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
// src/TrustCirclePanel.js
//
// UI for the wallet-personalized Reality Tunnel modes. Lets the connected
// wallet view the graph through its own on-chain attestations about other
// accounts, through a specific trusted account's perspective, or through the
// aggregated view of everyone in its circle.
import React, { useEffect, useState, useCallback } from "react";
import { createClient } from "./api";
import {
resolveAccountAtom,
fetchOutboundTrustEdges,
fetchAggregatedTrustCircle,
buildTrustCircleGraph,
buildGraphFromTriples,
} from "./trustCircle";

const MODES = [
{ id: "mine", label: "My Trust Circle" },
{ id: "peer", label: "Peer Perspective" },
{ id: "all", label: "All Trust Circle" },
];

const panelStyle = {
position: "absolute",
top: 80,
right: 16,
zIndex: 3,
background: "rgba(0,0,0,0.6)",
backdropFilter: "blur(4px)",
border: "1px solid rgba(255,255,255,0.1)",
borderRadius: 8,
padding: 12,
color: "#fff",
width: 300,
};

export default function TrustCirclePanel({ endpoint, connectedAddress, connectedLabel, onGraphData }) {
const [active, setActive] = useState(false);
const [mode, setMode] = useState("mine");
const [myCircle, setMyCircle] = useState([]); // [{address, label}] for the peer picker
const [peerAddress, setPeerAddress] = useState("");
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const [summary, setSummary] = useState("");

const reset = useCallback(() => {
setError(null);
setSummary("");
onGraphData(null);
}, [onGraphData]);

// Turning the trust tunnel off, or disconnecting the wallet, restores the
// normal graph view.
useEffect(() => {
if (!connectedAddress || !active) {
reset();
}
}, [connectedAddress, active, reset]);

useEffect(() => {
if (!active || !connectedAddress) return;
let cancelled = false;

const run = async () => {
setLoading(true);
setError(null);
try {
const client = createClient(endpoint);

if (mode === "mine") {
const root = await resolveAccountAtom(client, connectedAddress);
if (!root) {
if (!cancelled) {
setSummary("This wallet has no atom on-chain yet — nothing to build a trust circle from.");
onGraphData(null);
setMyCircle([]);
}
return;
}
const edges = await fetchOutboundTrustEdges(client, root.atomId, root.address);
if (cancelled) return;
setMyCircle(edges.map((e) => ({ address: e.target.address, label: e.target.label })));
if (edges.length === 0) {
setSummary(`${root.label} hasn't made any on-chain claims about other accounts yet.`);
onGraphData(null);
return;
}
setSummary(`${edges.length} account${edges.length === 1 ? "" : "s"} this wallet has attested about.`);
onGraphData(buildTrustCircleGraph(root, edges));
} else if (mode === "peer") {
if (!peerAddress) {
setSummary("Pick someone from your trust circle to view the graph through their perspective.");
onGraphData(null);
return;
}
const peer = await resolveAccountAtom(client, peerAddress);
if (!peer) {
setSummary("Could not resolve that account on-chain.");
onGraphData(null);
return;
}
const edges = await fetchOutboundTrustEdges(client, peer.atomId, peer.address);
if (cancelled) return;
if (edges.length === 0) {
setSummary(`${peer.label} hasn't made any on-chain claims about other accounts yet.`);
onGraphData(null);
return;
}
setSummary(`Viewing the graph through ${peer.label}'s ${edges.length} attestation${edges.length === 1 ? "" : "s"}.`);
onGraphData(buildTrustCircleGraph(peer, edges));
} else if (mode === "all") {
const { root, rootEdges, triples, edgeCount } = await fetchAggregatedTrustCircle(client, connectedAddress);
if (cancelled) return;
if (!root || edgeCount === 0) {
setSummary(
root
? `${root.label} hasn't made any on-chain claims about other accounts yet.`
: "This wallet has no atom on-chain yet — nothing to build a trust circle from."
);
onGraphData(null);
setMyCircle([]);
return;
}
setMyCircle(rootEdges.map((e) => ({ address: e.target.address, label: e.target.label })));
setSummary(
`Aggregated view: ${root.label}'s circle (${edgeCount} direct) plus who they trust, ${triples.length} claims total.`
);
onGraphData(buildGraphFromTriples(triples));
}
} catch (e) {
console.error("Error building trust circle graph:", e);
if (!cancelled) {
setError("Could not load trust circle data from mainnet.");
onGraphData(null);
}
} finally {
if (!cancelled) setLoading(false);
}
};

run();
return () => {
cancelled = true;
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [active, mode, peerAddress, connectedAddress, endpoint]);

if (!connectedAddress) return null;

return (
<section style={panelStyle}>
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}>
<h3 style={{ margin: 0, fontSize: 14 }}>Reality Tunnel: Trust Circle</h3>
<label style={{ fontSize: 12, display: "flex", alignItems: "center", gap: 6 }}>
<input type="checkbox" checked={active} onChange={(e) => setActive(e.target.checked)} />
Enabled
</label>
</div>

{active && (
<>
<div style={{ display: "flex", gap: 6, margin: "10px 0" }}>
{MODES.map((m) => (
<button
key={m.id}
className="navigation-button"
onClick={() => setMode(m.id)}
style={{
fontSize: 11,
padding: "4px 8px",
opacity: mode === m.id ? 1 : 0.6,
border: mode === m.id ? "1px solid #fff" : undefined,
}}
>
{m.label}
</button>
))}
</div>

{mode === "peer" && (
<select
className="tunnel-select"
value={peerAddress}
onChange={(e) => setPeerAddress(e.target.value)}
style={{ width: "100%", marginBottom: 8 }}
disabled={myCircle.length === 0}
>
<option value="">
{myCircle.length === 0 ? "No one in your circle yet" : "Select a person from your circle"}
</option>
{myCircle.map((p) => (
<option key={p.address} value={p.address}>
{p.label}
</option>
))}
</select>
)}

{loading && <p style={{ fontSize: 12, color: "#ccc" }}>Loading from mainnet…</p>}
{error && <p style={{ fontSize: 12, color: "#f88" }}>{error}</p>}
{!loading && !error && summary && <p style={{ fontSize: 12, color: "#ccc" }}>{summary}</p>}
</>
)}
</section>
);
}
4 changes: 2 additions & 2 deletions src/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ export const ENDPOINTS = {
module: BaseSepolia,
},
base: {
url: "https://testnet.intuition.sh/v1/graphql",
displayName: "Intuition Testnet",
url: "https://mainnet.intuition.sh/v1/graphql",
displayName: "Intuition Mainnet",
module: Base,
},
};
Expand Down
4 changes: 2 additions & 2 deletions src/api/Base.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import {

export const ENDPOINTS = {
base: {
url: "https://testnet.intuition.sh/v1/graphql",
displayName: "Intuition Testnet",
url: "https://mainnet.intuition.sh/v1/graphql",
displayName: "Intuition Mainnet",
},
};
// Create GraphQL client based on endpoint
Expand Down
Loading