feat: Make galaxy viewable without login requirement
Implemented public galaxy viewing feature that allows unauthenticated
users to view public thought galaxies via the ?user={did} parameter,
while maintaining privacy controls for node-level visibility.
Changes:
- Updated /api/galaxy/route.ts to support public access:
* Accept ?user={did} query parameter for viewing specific user's galaxy
* Show all nodes (including private) for authenticated user viewing own galaxy
* Filter to only public nodes when viewing someone else's galaxy
* Return empty state with helpful message when not authenticated
* Filter links to only show connections between visible nodes
- Added is_public field to database schema:
* Updated db/schema.surql with DEFAULT true (public by default)
* Created migration script scripts/add-is-public-field.ts
* Aligns with ATproto's public-by-default philosophy
- Enhanced ThoughtGalaxy component:
* Support viewing galaxies via ?user={did} parameter
* Display user info banner when viewing public galaxy
* Show appropriate empty state messages based on context
* Refetch data when user parameter changes
- Created comprehensive Magnitude tests:
* Test public galaxy viewing without authentication
* Verify private nodes are hidden from public view
* Test own galaxy access requires authentication
* Validate invalid user DID handling
* Test user info display and navigation between galaxies
- Documented implementation plan in plans/10-public-galaxy-viewing.md
This implements the "public by default" model while allowing future
node-level privacy controls. All canonical data remains on the user's
ATproto PDS, with SurrealDB serving as a high-performance cache.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -95,18 +95,25 @@ export function ThoughtGalaxy() {
|
||||
const [nodes, setNodes] = useState<NodeData[]>([]);
|
||||
const [links, setLinks] = useState<LinkData[]>([]);
|
||||
const [selectedNode, setSelectedNode] = useState<NodeData | null>(null);
|
||||
const [emptyMessage, setEmptyMessage] = useState<string | null>(null);
|
||||
const cameraControlsRef = useRef<CameraControls>(null);
|
||||
const hasFitCamera = useRef(false);
|
||||
const hasFocusedNode = useRef<string | null>(null);
|
||||
|
||||
// Get selectedNodeId from URL query params
|
||||
// Get query params
|
||||
const selectedNodeId = searchParams.get('node');
|
||||
const targetUserDid = searchParams.get('user'); // For viewing someone else's galaxy
|
||||
|
||||
// Fetch data from API on mount and poll for updates
|
||||
useEffect(() => {
|
||||
async function fetchData() {
|
||||
try {
|
||||
const response = await fetch('/api/galaxy', {
|
||||
// Build URL with optional user parameter
|
||||
const url = targetUserDid
|
||||
? `/api/galaxy?user=${encodeURIComponent(targetUserDid)}`
|
||||
: '/api/galaxy';
|
||||
|
||||
const response = await fetch(url, {
|
||||
credentials: 'include', // Include cookies for authentication
|
||||
});
|
||||
|
||||
@@ -119,13 +126,17 @@ export function ThoughtGalaxy() {
|
||||
|
||||
if (data.message) {
|
||||
console.log('[ThoughtGalaxy]', data.message);
|
||||
setEmptyMessage(data.message);
|
||||
// If calculating, poll again in 2 seconds
|
||||
setTimeout(fetchData, 2000);
|
||||
if (data.message.includes('calculating')) {
|
||||
setTimeout(fetchData, 2000);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
setNodes(data.nodes || []);
|
||||
setLinks(data.links || []);
|
||||
setEmptyMessage(null);
|
||||
|
||||
console.log(`[ThoughtGalaxy] Loaded ${data.nodes?.length || 0} nodes and ${data.links?.length || 0} links`);
|
||||
} catch (error) {
|
||||
@@ -134,7 +145,7 @@ export function ThoughtGalaxy() {
|
||||
}
|
||||
|
||||
fetchData();
|
||||
}, []);
|
||||
}, [targetUserDid]);
|
||||
|
||||
// Function to fit camera to all nodes
|
||||
const fitCameraToNodes = () => {
|
||||
@@ -286,17 +297,49 @@ export function ThoughtGalaxy() {
|
||||
return (
|
||||
<Stack align="center" justify="center" style={{ height: '100vh', width: '100vw' }}>
|
||||
<MantineText size="lg" c="dimmed">
|
||||
Create at least 3 nodes to visualize your thought galaxy
|
||||
</MantineText>
|
||||
<MantineText size="sm" c="dimmed">
|
||||
Nodes with content will automatically generate embeddings and 3D coordinates
|
||||
{emptyMessage || 'Create at least 3 nodes to visualize your thought galaxy'}
|
||||
</MantineText>
|
||||
{!emptyMessage && (
|
||||
<MantineText size="sm" c="dimmed">
|
||||
Nodes with content will automatically generate embeddings and 3D coordinates
|
||||
</MantineText>
|
||||
)}
|
||||
{targetUserDid && (
|
||||
<MantineText size="sm" c="dimmed" mt="xs">
|
||||
Viewing galaxy for user: {targetUserDid}
|
||||
</MantineText>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* User info banner when viewing someone else's galaxy */}
|
||||
{targetUserDid && (
|
||||
<Box
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '10px',
|
||||
right: '10px',
|
||||
zIndex: 999,
|
||||
maxWidth: '300px',
|
||||
}}
|
||||
>
|
||||
<Paper p="sm" radius="md" withBorder shadow="md">
|
||||
<MantineText size="sm" fw={600}>
|
||||
Public Galaxy
|
||||
</MantineText>
|
||||
<MantineText size="xs" c="dimmed">
|
||||
Viewing {nodes.length} public {nodes.length === 1 ? 'node' : 'nodes'}
|
||||
</MantineText>
|
||||
<MantineText size="xs" c="dimmed" style={{ wordBreak: 'break-all' }}>
|
||||
{targetUserDid}
|
||||
</MantineText>
|
||||
</Paper>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Floating content overlay for selected node */}
|
||||
{selectedNode && (
|
||||
<Box
|
||||
|
||||
Reference in New Issue
Block a user