* first attempt at stream sidebar, tasks, and events * fix folder from root * cleanup folders and events, and condense changes * cleanup and add toggle for sidebar * cleanup * fix nits
171 lines
5.1 KiB
TypeScript
171 lines
5.1 KiB
TypeScript
import { useEffect } from 'react';
|
|
import { useNavigate, useParams } from 'react-router-dom';
|
|
import { useQueryClient } from '@tanstack/react-query';
|
|
import { Lock } from 'lucide-react';
|
|
import { isParticleDeleted, type Particle } from '@/api/types';
|
|
import { useLiveParticle } from '@/hooks/use-particle';
|
|
import { particlePath, type ParticlePath } from '@/lib/particle-path';
|
|
import { Button } from '@/components/ui/button';
|
|
|
|
import Layout from '@/features/layout';
|
|
import { StreamView } from '@/features/particles/stream-view';
|
|
import { FolderView } from '@/features/particles/folder-view';
|
|
import { MediaParticleView } from '@/features/particles/media-particle-view';
|
|
import { TextParticleView } from '@/features/particles/text-particle-view';
|
|
import { TaskParticleView } from '@/features/particles/task-particle-view';
|
|
import { FallbackParticleView } from '@/features/particles/fallback-particle-view';
|
|
import { DeletedParticleView } from '@/features/particles/deleted-particle-view';
|
|
|
|
/**
|
|
* Route-level component for /:networkId/*.
|
|
* Reads params from the router, resolves the particle, and renders
|
|
* the appropriate view based on particle type.
|
|
*/
|
|
export default function ParticleViewResolver() {
|
|
const { networkId, '*': rest } = useParams();
|
|
if (!networkId)
|
|
throw new Error('ParticleViewResolver requires a :networkId route param');
|
|
const segments = (rest ?? '').split('/').filter(Boolean);
|
|
const path = particlePath(networkId, segments); // path of current container particle
|
|
|
|
const { particle, isLoading, error } = useLiveParticle(path);
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<Layout>
|
|
<div className="flex h-full items-center justify-center">
|
|
<p className="text-muted-foreground text-sm">Loading...</p>
|
|
</div>
|
|
</Layout>
|
|
);
|
|
}
|
|
|
|
if (error || !particle) {
|
|
// Errors here are almost always Firestore permission-denied — the user lost
|
|
// access to the network or to a custom-visibility particle. The React Router
|
|
// stays on the dead route, so without an explicit escape the user is stuck.
|
|
return (
|
|
<Layout>
|
|
<InaccessibleParticle />
|
|
</Layout>
|
|
);
|
|
}
|
|
|
|
// Streams render their own full-screen chrome; folders and leaves live
|
|
// inside the app Layout (breadcrumbs, full-height column) like the root.
|
|
switch (particle.type) {
|
|
case 'stream':
|
|
return <StreamView streamParticle={particle} path={path} />;
|
|
case 'folder':
|
|
return (
|
|
<Layout>
|
|
<FolderView folderParticle={particle} path={path} />
|
|
</Layout>
|
|
);
|
|
default:
|
|
return (
|
|
<Layout>
|
|
<LeafParticleView
|
|
particle={particle}
|
|
containerPath={particlePath(networkId, segments.slice(0, -1))}
|
|
networkId={networkId}
|
|
/>
|
|
</Layout>
|
|
);
|
|
}
|
|
}
|
|
|
|
const noop = () => {};
|
|
|
|
/**
|
|
* Standalone view for a leaf particle opened directly (e.g. from a folder),
|
|
* outside any stream playback: renders the particle's native view with no
|
|
* auto-advance.
|
|
*/
|
|
function LeafParticleView({
|
|
particle,
|
|
containerPath,
|
|
networkId,
|
|
}: {
|
|
particle: Particle;
|
|
containerPath: ParticlePath;
|
|
networkId: string;
|
|
}) {
|
|
const content = (() => {
|
|
if (isParticleDeleted(particle)) {
|
|
return (
|
|
<DeletedParticleView
|
|
particle={particle}
|
|
networkId={networkId}
|
|
paused
|
|
onEnded={noop}
|
|
/>
|
|
);
|
|
}
|
|
switch (particle.type) {
|
|
case 'media':
|
|
return (
|
|
<MediaParticleView
|
|
particle={particle}
|
|
streamPath={containerPath}
|
|
paused={false}
|
|
onEnded={noop}
|
|
/>
|
|
);
|
|
case 'text':
|
|
return (
|
|
<TextParticleView
|
|
particle={particle}
|
|
streamPath={containerPath}
|
|
paused
|
|
onEnded={noop}
|
|
/>
|
|
);
|
|
case 'task':
|
|
return (
|
|
<TaskParticleView
|
|
particle={particle}
|
|
containerPath={containerPath}
|
|
paused
|
|
onEnded={noop}
|
|
/>
|
|
);
|
|
default:
|
|
return (
|
|
<FallbackParticleView particle={particle} networkId={networkId} />
|
|
);
|
|
}
|
|
})();
|
|
|
|
return (
|
|
<div className="min-h-0 flex-1 bg-black text-white [--stream-safe-top:2rem] [--stream-safe-bottom:2rem]">
|
|
{content}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function InaccessibleParticle() {
|
|
const navigate = useNavigate();
|
|
const queryClient = useQueryClient();
|
|
|
|
useEffect(() => {
|
|
// Refresh the networks list so the home page reflects current access.
|
|
queryClient.invalidateQueries({ queryKey: ['networks'] });
|
|
}, [queryClient]);
|
|
|
|
return (
|
|
<div className="flex h-full flex-col items-center justify-center gap-4 p-8 text-center">
|
|
<Lock className="text-muted-foreground size-8" />
|
|
<div className="flex max-w-sm flex-col gap-1">
|
|
<p className="text-sm font-medium">This particle isn't available</p>
|
|
<p className="text-muted-foreground text-xs">
|
|
It may have been deleted, or your access was removed.
|
|
</p>
|
|
</div>
|
|
<Button size="sm" onClick={() => navigate('/', { replace: true })}>
|
|
Go home
|
|
</Button>
|
|
</div>
|
|
);
|
|
}
|