show indicator of stream expiring soon

This commit is contained in:
talksik
2026-03-30 13:33:02 -07:00
parent d56541bd13
commit d10de739b3
3 changed files with 52 additions and 10 deletions
@@ -9,6 +9,7 @@ import {
FileText, FileText,
CircleCheck, CircleCheck,
StickyNote, StickyNote,
Timer,
type LucideIcon, type LucideIcon,
} from "lucide-react"; } from "lucide-react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
@@ -26,6 +27,7 @@ import { Progress } from "@/components/ui/progress";
import { Small } from "@/components/ui/typography"; import { Small } from "@/components/ui/typography";
import type { Particle, StreamProperties } from "@/api/types"; import type { Particle, StreamProperties } from "@/api/types";
import { useNetwork } from "@/hooks/use-networks"; import { useNetwork } from "@/hooks/use-networks";
import { useExpiringSoon } from "@/hooks/use-expiring-soon";
import { useStreamParticles } from "@/hooks/use-stream-particles"; import { useStreamParticles } from "@/hooks/use-stream-particles";
import { useStreamAutoplay } from "@/hooks/use-stream-autoplay"; import { useStreamAutoplay } from "@/hooks/use-stream-autoplay";
@@ -94,6 +96,11 @@ function StreamRow({
useStreamAutoplay(latestChild, particle); useStreamAutoplay(latestChild, particle);
const expiringSoon = useExpiringSoon(
particle.last_child_created_at,
network?.message_retention_hours ?? 24,
);
const isDM = const isDM =
particle.visible_to.length === 2 && particle.visible_to.length === 2 &&
particle.visible_to.every((v) => v.startsWith("human:")); particle.visible_to.every((v) => v.startsWith("human:"));
@@ -181,16 +188,21 @@ function StreamRow({
> >
{particle.properties.name} {particle.properties.name}
</p> </p>
{latestChild && ( <div className="flex shrink-0 items-center gap-1">
<Small {expiringSoon && (
className={cn( <Timer className="size-3 text-muted-foreground/60" />
"shrink-0", )}
isUnseen ? "text-primary" : "text-muted-foreground", {latestChild && (
)} <Small
> className={cn(
<RelativeTimestamp date={latestChild.created_at} /> "shrink-0",
</Small> isUnseen ? "text-primary" : "text-muted-foreground",
)} )}
>
<RelativeTimestamp date={latestChild.created_at} />
</Small>
)}
</div>
</div> </div>
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<TypeIcon <TypeIcon
+10
View File
@@ -1,10 +1,12 @@
import { forwardRef, useMemo } from "react"; import { forwardRef, useMemo } from "react";
import { Timer } from "lucide-react";
import { cn, getInitials } from "@/lib/utils"; import { cn, getInitials } from "@/lib/utils";
import { useLiveLatestChild } from "@/hooks/use-particle"; import { useLiveLatestChild } from "@/hooks/use-particle";
import { useAuthStore } from "@/stores/auth-store"; import { useAuthStore } from "@/stores/auth-store";
import { particlePath } from "@/lib/particle-path"; import { particlePath } from "@/lib/particle-path";
import type { Particle, StreamProperties } from "@/api/types"; import type { Particle, StreamProperties } from "@/api/types";
import { useStreamAutoplay } from "@/hooks/use-stream-autoplay"; import { useStreamAutoplay } from "@/hooks/use-stream-autoplay";
import { useExpiringSoon } from "@/hooks/use-expiring-soon";
import { ParticlePreview } from "@/features/particles/particle-preview"; import { ParticlePreview } from "@/features/particles/particle-preview";
import { useNetwork } from "@/hooks/use-networks"; import { useNetwork } from "@/hooks/use-networks";
import { Avatar, AvatarFallback } from "@/components/ui/avatar"; import { Avatar, AvatarFallback } from "@/components/ui/avatar";
@@ -27,6 +29,11 @@ export const StreamCard = forwardRef<HTMLDivElement, StreamCardProps>(function S
useStreamAutoplay(latestChild, particle); useStreamAutoplay(latestChild, particle);
const expiringSoon = useExpiringSoon(
particle.last_child_created_at,
network?.message_retention_hours ?? 24,
);
const isDM = const isDM =
particle.visible_to.length === 2 && particle.visible_to.length === 2 &&
particle.visible_to.every((v) => v.startsWith("human:")); particle.visible_to.every((v) => v.startsWith("human:"));
@@ -134,6 +141,9 @@ export const StreamCard = forwardRef<HTMLDivElement, StreamCardProps>(function S
{particle.properties.name} {particle.properties.name}
</Small> </Small>
<div className="ml-auto flex shrink-0 items-center gap-1.5"> <div className="ml-auto flex shrink-0 items-center gap-1.5">
{expiringSoon && (
<Timer className="size-3 text-muted-foreground/60" />
)}
{latestChild && ( {latestChild && (
<Small <Small
className={cn( className={cn(
+20
View File
@@ -0,0 +1,20 @@
import { useMemo } from "react";
/**
* Returns true when a stream is within the last 10% of its retention window.
* For example, with 24h retention, this fires when < 2.4h remain.
*/
export function useExpiringSoon(
lastChildCreatedAt: Date | undefined,
retentionHours: number,
): boolean {
return useMemo(() => {
if (!lastChildCreatedAt) return false;
const retentionMs = retentionHours * 60 * 60 * 1000;
const expiresAt = lastChildCreatedAt.getTime() + retentionMs;
const remaining = expiresAt - Date.now();
return remaining > 0 && remaining < retentionMs * 0.1;
}, [lastChildCreatedAt, retentionHours]);
}