Couldn't load the feed.
;
- if (items.length === 0)
+ if (items.length === 0) {
+ if (!canRead)
+ return (
+ No videos match these filters.
;
+ }
return (
diff --git a/frontend/src/components/OnboardingWizard.tsx b/frontend/src/components/OnboardingWizard.tsx
index 2bc6981..51ac1dd 100644
--- a/frontend/src/components/OnboardingWizard.tsx
+++ b/frontend/src/components/OnboardingWizard.tsx
@@ -1,6 +1,7 @@
-import { useEffect } from "react";
-import { AlertTriangle, Check, ShieldCheck, X, Youtube } from "lucide-react";
-import type { Me } from "../lib/api";
+import { useEffect, useRef } from "react";
+import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
+import { AlertTriangle, Check, Loader2, RefreshCw, ShieldCheck, X, Youtube } from "lucide-react";
+import { api, type Me } from "../lib/api";
import { beginGrant, endOnboarding } from "../lib/onboarding";
// A first-login wizard that walks the user through granting YouTube access one scope at a
@@ -10,6 +11,8 @@ import { beginGrant, endOnboarding } from "../lib/onboarding";
//
// The visible step is derived from what's already granted, so the wizard resumes correctly
// after each redirect: no read -> "read", read but no write -> "write", both -> "done".
+// Right after read is granted, it auto-imports the user's subscriptions so they land on a
+// populated feed (channels already in the shared catalog appear instantly).
// A small reusable heads-up so the Google consent warning never feels like a surprise.
function ConsentHeadsUp() {
@@ -52,6 +55,42 @@ function Dots({ index, total }: { index: number; total: number }) {
}
export default function OnboardingWizard({ me, onClose }: { me: Me; onClose: () => void }) {
+ const qc = useQueryClient();
+ const importTriggered = useRef(false);
+
+ // Once we have read access, find out whether any subscriptions are imported yet.
+ const myStatus = useQuery({
+ queryKey: ["my-status"],
+ queryFn: api.myStatus,
+ enabled: me.can_read,
+ });
+
+ const importSubs = useMutation({
+ mutationFn: () => api.syncSubscriptions(),
+ onSuccess: () => {
+ // Feed/channels/status now have content — refresh the app behind the wizard.
+ qc.invalidateQueries({ queryKey: ["feed"] });
+ qc.invalidateQueries({ queryKey: ["feed-count"] });
+ qc.invalidateQueries({ queryKey: ["channels"] });
+ qc.invalidateQueries({ queryKey: ["my-status"] });
+ },
+ });
+
+ // First time we see read access with zero imported channels, pull the user's
+ // subscriptions automatically so they don't have to hunt for a button.
+ useEffect(() => {
+ if (
+ me.can_read &&
+ myStatus.data &&
+ myStatus.data.channels_total === 0 &&
+ !importTriggered.current
+ ) {
+ importTriggered.current = true;
+ importSubs.mutate();
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [me.can_read, myStatus.data?.channels_total]);
+
// Closing without finishing the essential read step suppresses future auto-popups
// (it stays reachable from Settings). Once read is granted, closing is just "done".
const close = () => {
@@ -68,6 +107,15 @@ export default function OnboardingWizard({ me, onClose }: { me: Me; onClose: ()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [me.can_read]);
+ // Are we currently importing (or about to)? Show a progress screen instead of the
+ // "you're connected" step until the first import settles.
+ const importing =
+ me.can_read &&
+ !importSubs.isError &&
+ (importSubs.isPending ||
+ (importTriggered.current && !importSubs.isSuccess) ||
+ (myStatus.data?.channels_total === 0 && !importSubs.isSuccess));
+
const step = !me.can_read ? "read" : !me.can_write ? "write" : "done";
const stepIndex = step === "read" ? 0 : step === "write" ? 1 : 2;
@@ -112,18 +160,53 @@ export default function OnboardingWizard({ me, onClose }: { me: Me; onClose: ()
>
)}
- {step === "write" && (
+ {/* Read granted: show import progress before the optional write step. */}
+ {me.can_read && importing && (
+ <>
+
+
+
+
Building your feed…
+
+ Importing your YouTube subscriptions. Channels already in Siftlode show up
+ instantly; any new ones fill in automatically in the background.
+
+ >
+ )}
+
+ {step === "write" && !importing && (
<>
You're connected
- Your feed is ready. Optionally, you can let Siftlode{" "}
- unsubscribe from channels on YouTube for you —
- this needs an extra write permission. Skip it to stay read-only; you can always
- enable it later in Settings.
+ {importSubs.isError ? (
+ <>Your YouTube account is connected, but importing subscriptions failed — you
+ can retry from Settings → Sync. >
+ ) : (
+ <>
+ Your feed is ready
+ {myStatus.data ? ` (${myStatus.data.channels_total} channels)` : ""}. Optionally,
+ let Siftlode{" "}
+ >
+ )}
+ {!importSubs.isError && (
+ <>
+ unsubscribe from channels on YouTube for you
+ — this needs an extra write permission. Skip it to stay read-only; you can
+ always enable it later in Settings.
+ >
+ )}
+ {importSubs.isError && (
+
+ )}
)}
- {step === "done" && (
+ {step === "done" && !importing && (
<>