cleanup message retention code
This commit is contained in:
@@ -178,15 +178,6 @@ class ApiClient {
|
||||
);
|
||||
}
|
||||
|
||||
async setMessageRetentionHours(networkId: string, hours: number) {
|
||||
return this.request(
|
||||
NetworkSchema,
|
||||
"PUT",
|
||||
`/networks/${networkId}/message-retention`,
|
||||
{ hours },
|
||||
);
|
||||
}
|
||||
|
||||
// --- Invitations ---
|
||||
|
||||
async listNetworkInvitations(networkId: string) {
|
||||
|
||||
+1
-1
@@ -14,7 +14,6 @@ export const NetworkSchema = z.object({
|
||||
name: z.string(),
|
||||
admin_human: HumanSchema,
|
||||
humans: z.array(HumanSchema),
|
||||
message_retention_hours: z.number(),
|
||||
created_at: z.coerce.date(),
|
||||
});
|
||||
|
||||
@@ -186,6 +185,7 @@ export const ParticleSchema = z.discriminatedUnion("type", [
|
||||
last_child_created_at: z.coerce.date().optional(),
|
||||
// Array of humanIds currently in the huddle (updated via LiveKit webhooks)
|
||||
huddle_active_participants: z.array(z.string()).optional(),
|
||||
status: z.enum(["open", "closed"]).optional(),
|
||||
}),
|
||||
ParticleBaseSchema.extend({
|
||||
type: z.literal("folder"), properties: FolderPropertiesSchema,
|
||||
|
||||
@@ -8,11 +8,9 @@ import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { Slider } from "@/components/ui/slider";
|
||||
import { Muted } from "@/components/ui/typography";
|
||||
import { WindowControls } from "@/components/window-controls";
|
||||
import { useNetworks } from "@/hooks/use-networks";
|
||||
import { useSetMessageRetention } from "@/hooks/use-network-settings";
|
||||
import {
|
||||
useNetworkInvitations,
|
||||
useInviteMembers,
|
||||
@@ -150,54 +148,6 @@ function SettingsGroup({
|
||||
);
|
||||
}
|
||||
|
||||
function formatRetentionDays(hours: number): string {
|
||||
const days = Math.round(hours / 24);
|
||||
return days === 1 ? "1 day" : `${days} days`;
|
||||
}
|
||||
|
||||
function EphemeralitySettings({ networkId, retentionHours }: { networkId: string; retentionHours: number }) {
|
||||
const setRetention = useSetMessageRetention(networkId);
|
||||
const [days, setDays] = useState(Math.round(retentionHours / 24));
|
||||
const debounceRef = useRef<ReturnType<typeof setTimeout>>(undefined);
|
||||
|
||||
// Sync local state if server value changes externally
|
||||
useEffect(() => {
|
||||
setDays(Math.round(retentionHours / 24));
|
||||
}, [retentionHours]);
|
||||
|
||||
const handleChange = useCallback((value: number[]) => {
|
||||
const newDays = value[0];
|
||||
setDays(newDays);
|
||||
|
||||
if (debounceRef.current) clearTimeout(debounceRef.current);
|
||||
debounceRef.current = setTimeout(() => {
|
||||
setRetention.mutate(newDays * 24, {
|
||||
onSuccess: () => toast.success("Retention window updated"),
|
||||
onError: (err) => toast.error(err.message || "Failed to update retention"),
|
||||
});
|
||||
}, 500);
|
||||
}, [setRetention]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3 px-4 py-3">
|
||||
<div className="flex items-baseline justify-between">
|
||||
<p className="text-sm font-medium">Messages disappear after</p>
|
||||
<p className="text-sm font-semibold">{formatRetentionDays(days * 24)}</p>
|
||||
</div>
|
||||
<Slider
|
||||
min={1}
|
||||
max={14}
|
||||
step={1}
|
||||
value={[days]}
|
||||
onValueChange={handleChange}
|
||||
/>
|
||||
<Muted className="text-xs">
|
||||
Older messages are no longer visible to anyone.
|
||||
</Muted>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function NetworkSettingsPage() {
|
||||
const navigate = useNavigate();
|
||||
const { networkId } = useParams<{ networkId: string }>();
|
||||
@@ -240,18 +190,6 @@ export default function NetworkSettingsPage() {
|
||||
))}
|
||||
</SettingsGroup>
|
||||
|
||||
{isAdmin && network && (
|
||||
<>
|
||||
<Separator className="mt-4" />
|
||||
<SettingsGroup title="Ephemerality">
|
||||
<EphemeralitySettings
|
||||
networkId={networkId!}
|
||||
retentionHours={network.message_retention_hours}
|
||||
/>
|
||||
</SettingsGroup>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Separator className="mt-4" />
|
||||
|
||||
{isAdmin && network && (
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
FileText,
|
||||
CircleCheck,
|
||||
StickyNote,
|
||||
Timer,
|
||||
Headphones,
|
||||
type LucideIcon,
|
||||
} from "lucide-react";
|
||||
@@ -28,7 +27,6 @@ import { Progress } from "@/components/ui/progress";
|
||||
import { Small } from "@/components/ui/typography";
|
||||
import type { Particle, StreamProperties } from "@/api/types";
|
||||
import { useNetwork } from "@/hooks/use-networks";
|
||||
import { useExpiringSoon } from "@/hooks/use-expiring-soon";
|
||||
import { useStreamParticles } from "@/hooks/use-stream-particles";
|
||||
import { useStreamAutoplay } from "@/hooks/use-stream-autoplay";
|
||||
|
||||
@@ -97,11 +95,6 @@ function StreamRow({
|
||||
|
||||
useStreamAutoplay(latestChild, particle, networkId, network ?? undefined);
|
||||
|
||||
const expiringSoon = useExpiringSoon(
|
||||
particle.last_child_created_at,
|
||||
network?.message_retention_hours ?? 24,
|
||||
);
|
||||
|
||||
const hasActiveHuddle =
|
||||
particle.huddle_active_participants && particle.huddle_active_participants.length > 0;
|
||||
const huddleCount = particle.huddle_active_participants?.length ?? 0;
|
||||
@@ -201,9 +194,6 @@ function StreamRow({
|
||||
<span className="text-[10px] font-medium text-red-400">{huddleCount}</span>
|
||||
</span>
|
||||
)}
|
||||
{expiringSoon && (
|
||||
<Timer className="size-3 text-muted-foreground/60" />
|
||||
)}
|
||||
{latestChild && (
|
||||
<Small
|
||||
className={cn(
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import { forwardRef, useMemo } from "react";
|
||||
import { Timer, Headphones } from "lucide-react";
|
||||
import { Headphones } from "lucide-react";
|
||||
import { cn, getInitials } from "@/lib/utils";
|
||||
import { useLiveLatestChild } from "@/hooks/use-particle";
|
||||
import { useAuthStore } from "@/stores/auth-store";
|
||||
import { particlePath } from "@/lib/particle-path";
|
||||
import type { Particle, StreamProperties } from "@/api/types";
|
||||
import { useStreamAutoplay } from "@/hooks/use-stream-autoplay";
|
||||
import { useExpiringSoon } from "@/hooks/use-expiring-soon";
|
||||
import { ParticlePreview } from "@/features/particles/particle-preview";
|
||||
import { useNetwork } from "@/hooks/use-networks";
|
||||
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
|
||||
@@ -29,11 +28,6 @@ export const StreamCard = forwardRef<HTMLDivElement, StreamCardProps>(function S
|
||||
|
||||
useStreamAutoplay(latestChild, particle, networkId, network ?? undefined);
|
||||
|
||||
const expiringSoon = useExpiringSoon(
|
||||
particle.last_child_created_at,
|
||||
network?.message_retention_hours ?? 24,
|
||||
);
|
||||
|
||||
const hasActiveHuddle =
|
||||
particle.huddle_active_participants && particle.huddle_active_participants.length > 0;
|
||||
const huddleCount = particle.huddle_active_participants?.length ?? 0;
|
||||
@@ -155,9 +149,6 @@ export const StreamCard = forwardRef<HTMLDivElement, StreamCardProps>(function S
|
||||
<span className="text-[10px] font-medium text-red-400">{huddleCount}</span>
|
||||
</span>
|
||||
)}
|
||||
{expiringSoon && (
|
||||
<Timer className="size-3 text-muted-foreground/60" />
|
||||
)}
|
||||
{latestChild && (
|
||||
<Small
|
||||
className={cn(
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
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]);
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { apiClient } from "@/api/client";
|
||||
|
||||
export function useSetMessageRetention(networkId: string) {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (hours: number) =>
|
||||
apiClient.setMessageRetentionHours(networkId, hours),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["networks"] });
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -4,8 +4,6 @@ import type { Particle } from "@/api/types";
|
||||
import { useLiveParticleChildren } from "@/hooks/use-particle";
|
||||
import { parseParticlePath, toFirestoreDocPath, type ParticlePath } from "@/lib/particle-path";
|
||||
import { updateStreamPlaybackMarker } from "@/lib/firestore-particles";
|
||||
import { where, Timestamp } from "firebase/firestore";
|
||||
import { useNetwork } from "@/hooks/use-networks";
|
||||
|
||||
// --- Playback reducer (ID-based) ---
|
||||
|
||||
@@ -96,9 +94,6 @@ export function useStreamPlayback(
|
||||
path: ParticlePath,
|
||||
): UseStreamPlaybackResult {
|
||||
const userId = useAuthStore((s) => s.user?.id);
|
||||
const { networkId } = parseParticlePath(path);
|
||||
const network = useNetwork(networkId);
|
||||
const retentionHours = network?.message_retention_hours ?? 24;
|
||||
const [state, dispatch] = useReducer(playbackReducer, initialState);
|
||||
// Track the stream ID we've initialized for, to reset when navigating between streams
|
||||
const initializedForRef = useRef<string | null>(null);
|
||||
@@ -118,15 +113,14 @@ export function useStreamPlayback(
|
||||
});
|
||||
});
|
||||
|
||||
const [recencyCutoff] = useState(() => {
|
||||
const d = new Date();
|
||||
d.setHours(d.getHours() - retentionHours);
|
||||
return Timestamp.fromDate(d);
|
||||
});
|
||||
|
||||
const { children } = useLiveParticleChildren(
|
||||
path, "created_at", "asc", undefined, onParticleAdded, onParticleRemoved,
|
||||
where("created_at", ">=", recencyCutoff),
|
||||
path,
|
||||
{
|
||||
orderByField: "created_at",
|
||||
orderDirection: "asc",
|
||||
onAdded: onParticleAdded,
|
||||
onRemoved: onParticleRemoved
|
||||
}
|
||||
);
|
||||
|
||||
// Derive current index and particle from ID
|
||||
|
||||
Reference in New Issue
Block a user