improve window management and title bar

This commit is contained in:
talksik
2026-02-21 12:42:43 -08:00
parent 59b973802d
commit 8546260493
26 changed files with 229 additions and 25 deletions
@@ -14,13 +14,14 @@ function AudioLevelBars({ mediaStream }: { mediaStream: MediaStream }) {
useEffect(() => {
const ctx = new AudioContext();
ctx.resume();
const source = ctx.createMediaStreamSource(mediaStream);
const analyser = ctx.createAnalyser();
analyser.fftSize = 256;
source.connect(analyser);
audioRef.current = { analyser, ctx };
const dataArray = new Uint8Array(analyser.fftSize);
const dataArray = new Uint8Array(analyser.frequencyBinCount);
function tick() {
analyser.getByteTimeDomainData(dataArray);
@@ -0,0 +1,93 @@
import type { StreamParticle } from "@/api/types";
import { getParticleData } from "@/api/types";
import {
MessageSquare,
Video,
Mic,
ScrollText,
BookOpen,
FileIcon,
FolderIcon,
} from "lucide-react";
interface ParticlePreviewProps {
particle: StreamParticle;
}
export function ParticlePreview({ particle }: ParticlePreviewProps) {
switch (particle.type) {
case "text": {
const data = getParticleData(particle, "text");
return (
<div className="flex items-start gap-2">
<MessageSquare className="text-muted-foreground mt-0.5 h-3.5 w-3.5 shrink-0" />
<p className="text-muted-foreground line-clamp-2 text-xs">
{data.content}
</p>
</div>
);
}
case "media": {
const data = getParticleData(particle, "media");
const isVideo = data.mime_type.startsWith("video");
const Icon = isVideo ? Video : Mic;
const label = isVideo ? "Video" : "Audio";
const seconds = Math.round(data.duration_ms / 1000);
return (
<div className="flex items-center gap-2">
<Icon className="text-muted-foreground h-3.5 w-3.5 shrink-0" />
<span className="text-muted-foreground text-xs">
{label} · {seconds}s
</span>
</div>
);
}
case "quest": {
const data = getParticleData(particle, "quest");
return (
<div className="flex items-center gap-2">
<ScrollText className="text-muted-foreground h-3.5 w-3.5 shrink-0" />
<span className="text-muted-foreground truncate text-xs">
{data.title}
{data.status && ` · ${data.status}`}
</span>
</div>
);
}
case "paper": {
const data = getParticleData(particle, "paper");
return (
<div className="flex items-center gap-2">
<BookOpen className="text-muted-foreground h-3.5 w-3.5 shrink-0" />
<span className="text-muted-foreground truncate text-xs">
{data.title}
</span>
</div>
);
}
case "file": {
const data = getParticleData(particle, "file");
return (
<div className="flex items-center gap-2">
<FileIcon className="text-muted-foreground h-3.5 w-3.5 shrink-0" />
<span className="text-muted-foreground truncate text-xs">
{data.filename}
</span>
</div>
);
}
case "folder": {
const data = getParticleData(particle, "folder");
return (
<div className="flex items-center gap-2">
<FolderIcon className="text-muted-foreground h-3.5 w-3.5 shrink-0" />
<span className="text-muted-foreground truncate text-xs">
{data.name}
</span>
</div>
);
}
default:
return null;
}
}
+34 -17
View File
@@ -1,8 +1,10 @@
import { useNavigate } from "react-router-dom";
import { Badge } from "@/components/ui/badge";
import { Card, CardContent } from "@/components/ui/card";
import { useAppStore } from "@/stores/app-store";
import { flattenStreams } from "@/lib/stream-utils";
import { formatDistanceToNow } from "@/lib/time-utils";
import { ParticlePreview } from "./particle-preview";
export function StreamList() {
const networks = useAppStore((s) => s.networks);
@@ -20,22 +22,35 @@ export function StreamList() {
}
return (
<div className="flex flex-col">
<div className="grid grid-cols-2 gap-3 p-4">
{streams.map((stream) => {
const lastParticle =
stream.particles.length > 0
? stream.particles[stream.particles.length - 1]
: null;
const timeSource = lastParticle?.created_at ?? stream.updated_at;
return (
<button
<Card
key={stream.id}
size="sm"
className="hover:bg-accent/50 cursor-pointer transition-colors"
onClick={() => navigate(`/streams/${stream.id}`)}
className="hover:bg-accent/50 flex items-center gap-3 border-b px-4 py-3 text-left transition-colors"
>
<div className="min-w-0 flex-1">
<CardContent className="min-h-[3rem]">
{lastParticle ? (
<ParticlePreview particle={lastParticle} />
) : (
<p className="text-muted-foreground text-xs italic">
No messages yet
</p>
)}
</CardContent>
<CardContent className="pt-0">
<div className="flex items-center gap-2">
<span className="truncate text-sm font-medium">
<span className="min-w-0 truncate text-sm font-medium">
{stream.name}
</span>
{stream.unseen_count > 0 && (
@@ -44,18 +59,20 @@ export function StreamList() {
</Badge>
)}
</div>
{stream.description && (
<p className="text-muted-foreground mt-0.5 truncate text-xs">
{stream.description}
</p>
)}
{lastParticle && (
<div className="text-muted-foreground mt-1 text-xs">
{formatDistanceToNow(lastParticle.created_at)}
</div>
)}
</div>
</button>
</CardContent>
<CardContent className="pt-0">
<p className="text-muted-foreground truncate text-xs">
{lastParticle && (
<>
{lastParticle.created_by_email.split("@")[0]}
{" · "}
</>
)}
{timeSource && formatDistanceToNow(timeSource)}
</p>
</CardContent>
</Card>
);
})}
</div>