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 ---
|
// --- Invitations ---
|
||||||
|
|
||||||
async listNetworkInvitations(networkId: string) {
|
async listNetworkInvitations(networkId: string) {
|
||||||
|
|||||||
+1
-1
@@ -14,7 +14,6 @@ export const NetworkSchema = z.object({
|
|||||||
name: z.string(),
|
name: z.string(),
|
||||||
admin_human: HumanSchema,
|
admin_human: HumanSchema,
|
||||||
humans: z.array(HumanSchema),
|
humans: z.array(HumanSchema),
|
||||||
message_retention_hours: z.number(),
|
|
||||||
created_at: z.coerce.date(),
|
created_at: z.coerce.date(),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -186,6 +185,7 @@ export const ParticleSchema = z.discriminatedUnion("type", [
|
|||||||
last_child_created_at: z.coerce.date().optional(),
|
last_child_created_at: z.coerce.date().optional(),
|
||||||
// Array of humanIds currently in the huddle (updated via LiveKit webhooks)
|
// Array of humanIds currently in the huddle (updated via LiveKit webhooks)
|
||||||
huddle_active_participants: z.array(z.string()).optional(),
|
huddle_active_participants: z.array(z.string()).optional(),
|
||||||
|
status: z.enum(["open", "closed"]).optional(),
|
||||||
}),
|
}),
|
||||||
ParticleBaseSchema.extend({
|
ParticleBaseSchema.extend({
|
||||||
type: z.literal("folder"), properties: FolderPropertiesSchema,
|
type: z.literal("folder"), properties: FolderPropertiesSchema,
|
||||||
|
|||||||
@@ -8,11 +8,9 @@ import { Button } from "@/components/ui/button";
|
|||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
import { Slider } from "@/components/ui/slider";
|
|
||||||
import { Muted } from "@/components/ui/typography";
|
import { Muted } from "@/components/ui/typography";
|
||||||
import { WindowControls } from "@/components/window-controls";
|
import { WindowControls } from "@/components/window-controls";
|
||||||
import { useNetworks } from "@/hooks/use-networks";
|
import { useNetworks } from "@/hooks/use-networks";
|
||||||
import { useSetMessageRetention } from "@/hooks/use-network-settings";
|
|
||||||
import {
|
import {
|
||||||
useNetworkInvitations,
|
useNetworkInvitations,
|
||||||
useInviteMembers,
|
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() {
|
export default function NetworkSettingsPage() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { networkId } = useParams<{ networkId: string }>();
|
const { networkId } = useParams<{ networkId: string }>();
|
||||||
@@ -240,18 +190,6 @@ export default function NetworkSettingsPage() {
|
|||||||
))}
|
))}
|
||||||
</SettingsGroup>
|
</SettingsGroup>
|
||||||
|
|
||||||
{isAdmin && network && (
|
|
||||||
<>
|
|
||||||
<Separator className="mt-4" />
|
|
||||||
<SettingsGroup title="Ephemerality">
|
|
||||||
<EphemeralitySettings
|
|
||||||
networkId={networkId!}
|
|
||||||
retentionHours={network.message_retention_hours}
|
|
||||||
/>
|
|
||||||
</SettingsGroup>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Separator className="mt-4" />
|
<Separator className="mt-4" />
|
||||||
|
|
||||||
{isAdmin && network && (
|
{isAdmin && network && (
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import {
|
|||||||
FileText,
|
FileText,
|
||||||
CircleCheck,
|
CircleCheck,
|
||||||
StickyNote,
|
StickyNote,
|
||||||
Timer,
|
|
||||||
Headphones,
|
Headphones,
|
||||||
type LucideIcon,
|
type LucideIcon,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
@@ -28,7 +27,6 @@ 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";
|
||||||
|
|
||||||
@@ -97,11 +95,6 @@ function StreamRow({
|
|||||||
|
|
||||||
useStreamAutoplay(latestChild, particle, networkId, network ?? undefined);
|
useStreamAutoplay(latestChild, particle, networkId, network ?? undefined);
|
||||||
|
|
||||||
const expiringSoon = useExpiringSoon(
|
|
||||||
particle.last_child_created_at,
|
|
||||||
network?.message_retention_hours ?? 24,
|
|
||||||
);
|
|
||||||
|
|
||||||
const hasActiveHuddle =
|
const hasActiveHuddle =
|
||||||
particle.huddle_active_participants && particle.huddle_active_participants.length > 0;
|
particle.huddle_active_participants && particle.huddle_active_participants.length > 0;
|
||||||
const huddleCount = 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 className="text-[10px] font-medium text-red-400">{huddleCount}</span>
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{expiringSoon && (
|
|
||||||
<Timer className="size-3 text-muted-foreground/60" />
|
|
||||||
)}
|
|
||||||
{latestChild && (
|
{latestChild && (
|
||||||
<Small
|
<Small
|
||||||
className={cn(
|
className={cn(
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
import { forwardRef, useMemo } from "react";
|
import { forwardRef, useMemo } from "react";
|
||||||
import { Timer, Headphones } from "lucide-react";
|
import { Headphones } 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";
|
||||||
@@ -29,11 +28,6 @@ export const StreamCard = forwardRef<HTMLDivElement, StreamCardProps>(function S
|
|||||||
|
|
||||||
useStreamAutoplay(latestChild, particle, networkId, network ?? undefined);
|
useStreamAutoplay(latestChild, particle, networkId, network ?? undefined);
|
||||||
|
|
||||||
const expiringSoon = useExpiringSoon(
|
|
||||||
particle.last_child_created_at,
|
|
||||||
network?.message_retention_hours ?? 24,
|
|
||||||
);
|
|
||||||
|
|
||||||
const hasActiveHuddle =
|
const hasActiveHuddle =
|
||||||
particle.huddle_active_participants && particle.huddle_active_participants.length > 0;
|
particle.huddle_active_participants && particle.huddle_active_participants.length > 0;
|
||||||
const huddleCount = 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 className="text-[10px] font-medium text-red-400">{huddleCount}</span>
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{expiringSoon && (
|
|
||||||
<Timer className="size-3 text-muted-foreground/60" />
|
|
||||||
)}
|
|
||||||
{latestChild && (
|
{latestChild && (
|
||||||
<Small
|
<Small
|
||||||
className={cn(
|
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 { useLiveParticleChildren } from "@/hooks/use-particle";
|
||||||
import { parseParticlePath, toFirestoreDocPath, type ParticlePath } from "@/lib/particle-path";
|
import { parseParticlePath, toFirestoreDocPath, type ParticlePath } from "@/lib/particle-path";
|
||||||
import { updateStreamPlaybackMarker } from "@/lib/firestore-particles";
|
import { updateStreamPlaybackMarker } from "@/lib/firestore-particles";
|
||||||
import { where, Timestamp } from "firebase/firestore";
|
|
||||||
import { useNetwork } from "@/hooks/use-networks";
|
|
||||||
|
|
||||||
// --- Playback reducer (ID-based) ---
|
// --- Playback reducer (ID-based) ---
|
||||||
|
|
||||||
@@ -96,9 +94,6 @@ export function useStreamPlayback(
|
|||||||
path: ParticlePath,
|
path: ParticlePath,
|
||||||
): UseStreamPlaybackResult {
|
): UseStreamPlaybackResult {
|
||||||
const userId = useAuthStore((s) => s.user?.id);
|
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);
|
const [state, dispatch] = useReducer(playbackReducer, initialState);
|
||||||
// Track the stream ID we've initialized for, to reset when navigating between streams
|
// Track the stream ID we've initialized for, to reset when navigating between streams
|
||||||
const initializedForRef = useRef<string | null>(null);
|
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(
|
const { children } = useLiveParticleChildren(
|
||||||
path, "created_at", "asc", undefined, onParticleAdded, onParticleRemoved,
|
path,
|
||||||
where("created_at", ">=", recencyCutoff),
|
{
|
||||||
|
orderByField: "created_at",
|
||||||
|
orderDirection: "asc",
|
||||||
|
onAdded: onParticleAdded,
|
||||||
|
onRemoved: onParticleRemoved
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// Derive current index and particle from ID
|
// Derive current index and particle from ID
|
||||||
|
|||||||
Reference in New Issue
Block a user