Files
llink/js/src/hooks/use-stream-autoplay.ts
T
Claude 795bdfc286 refactor(errors): route silent catches through logError/reportError
Every catch now either surfaces, re-throws, or calls logError with a scope
tag. No more empty catches or bare console.error:

- auth-store: signInToFirebase / restoreSession / signOut paths gain
  logError context. Behavior is unchanged (best-effort local sign-out,
  fall back to login on restore failure).
- use-stream-autoplay: Audio.play() and download-URL fetches log their
  failures instead of dropping silently (both are nice-to-haves so UX
  stays silent — but we can now trace "why didn't autoplay trigger?").
- pusher-client: ws errors / parse failures / server errors / listener
  crashes all routed through logError, and listener bugs (which silently
  break user flows) now go through reportError so they're actually
  surfaced in observability.
- settings-page: email-notifications toggle now toasts on failure
  instead of silently reverting with no explanation.
- huddle-app: screen-share failures use logError.
2026-04-17 02:55:23 +00:00

73 lines
2.5 KiB
TypeScript

import { useEffect, useRef } from "react";
import beepSound from "../../assets/sound.wav";
import type { Network, Particle, StreamProperties } from "@/api/types";
import { apiClient } from "@/api/client";
import { useAuthStore } from "@/stores/auth-store";
import { useAutoplayStore } from "@/stores/autoplay-store";
import { resolveHumanDisplay } from "@/lib/humans";
import { logError } from "@/lib/errors";
/**
* Triggers autoplay when a stream's latest child changes to a new media particle.
* Plays a beep sound for new text particles from other users.
* Sends autoplay data to the separate autoplay window via IPC.
*/
export function useStreamAutoplay(
latestChild: Particle | null,
streamParticle: Particle & { type: "stream"; properties: StreamProperties },
networkId: string,
network: Network | undefined,
) {
const userId = useAuthStore((s) => s.user?.id) ?? "";
const settledIdRef = useRef<string | undefined>(undefined);
useEffect(() => {
if (!latestChild) return;
// First real value: record as baseline, don't autoplay
if (settledIdRef.current === undefined) {
settledIdRef.current = latestChild.id;
return;
}
if (latestChild.id === settledIdRef.current) return;
settledIdRef.current = latestChild.id;
if (latestChild.created_by_human_id === userId) return;
if (useAutoplayStore.getState().muted) return;
if (latestChild.type === "text") {
// Browser autoplay policy can block this before user interaction; that's
// fine — the beep is a nice-to-have, not a critical signal.
new Audio(beepSound).play().catch((err) =>
logError(err, { scope: "autoplay.beep" }),
);
return;
}
if (latestChild.type !== "media") return;
const particle = latestChild;
const { displayName, initials } = resolveHumanDisplay(
particle.created_by_human_id,
network?.humans,
);
apiClient.getParticleDownloadUrl(particle.properties.object_id).then((downloadUrl) => {
window.electronAutoplay.play({
particleId: particle.id,
streamId: streamParticle.id,
networkId,
downloadUrl,
mimeType: particle.properties.mime_type,
durationMs: particle.properties.duration_ms,
senderName: displayName,
senderInitials: initials,
});
}).catch((err) =>
logError(err, { scope: "autoplay.fetchUrl", particleId: particle.id }),
);
}, [latestChild?.id]); // eslint-disable-line react-hooks/exhaustive-deps
}