improve window management and title bar
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user