From 35c02e633641ccf2674bf12e12baf32037809cbc Mon Sep 17 00:00:00 2001 From: Eli Mallon Date: Sun, 4 Oct 2026 15:20:39 -0700 Subject: [PATCH] auto-vod: move compact publishing checkbox into stream form --- .maestro/logged-in/auto-publish-vods.yaml | 17 +++++-- .../live-dashboard/livestream-panel.tsx | 50 ++++++++++++++++++- .../live-dashboard/multistream-status.tsx | 48 +----------------- js/e2e-web/README.md | 12 ++--- js/e2e-web/flows/auto-publish-vods.spec.ts | 23 +++++++-- js/i18n/locales/en-US/settings.ftl | 4 +- js/i18n/public/locales/en-US/settings.json | 4 +- js/web/public/locales/en-US/settings.json | 4 +- 8 files changed, 95 insertions(+), 67 deletions(-) diff --git a/.maestro/logged-in/auto-publish-vods.yaml b/.maestro/logged-in/auto-publish-vods.yaml index fb167740..1936e064 100644 --- a/.maestro/logged-in/auto-publish-vods.yaml +++ b/.maestro/logged-in/auto-publish-vods.yaml @@ -31,14 +31,25 @@ appId: ${APP_ID} id: "settings-auto-publish-vods" checked: false timeout: 10000 +# The shared stream form exposes the same preference in its Create tab. +- openLink: "${APP_ID}://widgets/livestream" +- scrollUntilVisible: + element: + id: "live-auto-publish-vods" +- extendedWaitUntil: + visible: + id: "live-auto-publish-vods" + checked: false + enabled: true + timeout: 30000 - tapOn: - id: "settings-auto-publish-vods" + id: "live-auto-publish-vods" - extendedWaitUntil: visible: - id: "settings-auto-publish-vods" + id: "live-auto-publish-vods" checked: true timeout: 10000 -- takeScreenshot: auto-publish-vods-on +- takeScreenshot: auto-publish-vods-stream-form-on - launchApp - tapOn: text: "Settings" diff --git a/js/app/components/live-dashboard/livestream-panel.tsx b/js/app/components/live-dashboard/livestream-panel.tsx index e3ebb0ad..89bd8d9e 100644 --- a/js/app/components/live-dashboard/livestream-panel.tsx +++ b/js/app/components/live-dashboard/livestream-panel.tsx @@ -17,6 +17,7 @@ import { Text, Textarea, Tooltip, + useBetaStatus, useCreateStreamRecord, useEndLivestream, useLivestream, @@ -38,6 +39,7 @@ import { import { Image } from "expo-image"; import { ChevronsUpDown, Globe, ImagePlus, Lock, X } from "lucide-react-native"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { useTranslation } from "react-i18next"; import { Platform, Pressable, @@ -46,7 +48,7 @@ import { View, } from "react-native"; import { useStore } from "store"; -import { useUserProfile } from "store/hooks"; +import { useServerSettings, useUserProfile } from "store/hooks"; import type { place } from "streamplace"; import { SCOPE_BSKY_POST_CREATE, @@ -282,6 +284,37 @@ function LivestreamPanel({ scrollable = true }: { scrollable?: boolean }) { const endLivestream = useEndLivestream(); const url = useUrl(); const [endingLivestream, setEndingLivestream] = useState(false); + const { t } = useTranslation("settings"); + const agent = useStore((state) => state.pdsAgent); + const { status: vodBetaStatus } = useBetaStatus("vod"); + const serverSettings = useServerSettings(); + const getServerSettingsFromPDS = useStore( + (state) => state.getServerSettingsFromPDS, + ); + const createServerSettingsRecord = useStore( + (state) => state.createServerSettingsRecord, + ); + const [settingsLoading, setSettingsLoading] = useState(true); + const [saving, setSaving] = useState(false); + const savingRef = useRef(false); + + useEffect(() => { + if (!agent || vodBetaStatus !== "granted") return; + setSettingsLoading(true); + getServerSettingsFromPDS().finally(() => setSettingsLoading(false)); + }, [agent, url, vodBetaStatus, getServerSettingsFromPDS]); + + const setAutoPublishVods = async (autoPublishVods: boolean) => { + if (!agent || settingsLoading || savingRef.current) return; + savingRef.current = true; + setSaving(true); + try { + await createServerSettingsRecord({ autoPublishVods }); + } finally { + savingRef.current = false; + setSaving(false); + } + }; const initializedRef = useRef(false); const [title, setTitle] = useState(""); @@ -529,6 +562,7 @@ function LivestreamPanel({ scrollable = true }: { scrollable?: boolean }) { return ( + {vodBetaStatus === "granted" && ( + + + + )} diff --git a/js/app/components/live-dashboard/multistream-status.tsx b/js/app/components/live-dashboard/multistream-status.tsx index edbc4e6e..9fe9fe7e 100644 --- a/js/app/components/live-dashboard/multistream-status.tsx +++ b/js/app/components/live-dashboard/multistream-status.tsx @@ -1,12 +1,10 @@ import { useNavigation } from "@react-navigation/native"; import { Button, - Checkbox, Loader, Switch, Text, View, - useBetaStatus, zero, } from "@streamplace/components"; import { @@ -16,7 +14,7 @@ import { } from "@streamplace/components/src/lib/theme/tokens"; import { usePDSAgent } from "@streamplace/components/src/streamplace-store/xrpc"; import { Plus } from "lucide-react-native"; -import { useCallback, useEffect, useRef, useState } from "react"; +import { useCallback, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import Animated, { cancelAnimation, @@ -25,8 +23,6 @@ import Animated, { withRepeat, withTiming, } from "react-native-reanimated"; -import { useStore } from "store"; -import { useServerSettings, useStreamplaceUrl } from "store/hooks"; import { place } from "streamplace"; const { flex, p, gap, layout, bg, borders, text, r } = zero; @@ -40,36 +36,6 @@ export default function MultistreamStatus() { const agent = usePDSAgent(); const navigation = useNavigation(); const { t } = useTranslation("settings"); - const { status: vodBetaStatus } = useBetaStatus("vod"); - const serverSettings = useServerSettings(); - const url = useStreamplaceUrl(); - const getServerSettingsFromPDS = useStore( - (state) => state.getServerSettingsFromPDS, - ); - const createServerSettingsRecord = useStore( - (state) => state.createServerSettingsRecord, - ); - const [settingsLoading, setSettingsLoading] = useState(true); - const [saving, setSaving] = useState(false); - const savingRef = useRef(false); - - useEffect(() => { - if (!agent || vodBetaStatus !== "granted") return; - setSettingsLoading(true); - getServerSettingsFromPDS().finally(() => setSettingsLoading(false)); - }, [agent, url, vodBetaStatus, getServerSettingsFromPDS]); - - const setAutoPublishVods = async (autoPublishVods: boolean) => { - if (!agent || settingsLoading || savingRef.current) return; - savingRef.current = true; - setSaving(true); - try { - await createServerSettingsRecord({ autoPublishVods }); - } finally { - savingRef.current = false; - setSaving(false); - } - }; const [targets, setTargets] = useState([]); const [loading, setLoading] = useState(true); const [togglingTargets, setTogglingTargets] = useState>( @@ -195,18 +161,6 @@ export default function MultistreamStatus() { { borderColor: borderAlphas.dark.strong }, ]} > - {vodBetaStatus === "granted" && ( - - - - )} {loading && targets.length === 0 ? ( diff --git a/js/e2e-web/README.md b/js/e2e-web/README.md index 36b1a3cd..3feae8bb 100644 --- a/js/e2e-web/README.md +++ b/js/e2e-web/README.md @@ -58,12 +58,12 @@ fullscreen entry/exit, and verifies that faded controls reveal instead of accepting an unseen tap. `auto-publish-vods` saves and reloads the automatic publishing preference, -checks that the desktop live dashboard and Privacy & Security share it, and -hides the dashboard checkbox without VOD beta access. The checkbox explains that -consent also applies to future recorded streams until disabled. The destination panel is -absent from the native/narrow dashboard layout, so this dashboard coverage is -web-only; the native settings flow remains in Maestro. The browser flow also -holds a recording opt-out request to verify another privacy toggle cannot +checks that the stream form's Create tab and Privacy & Security share it, +including the popout form, and hides the control without VOD beta access or +outside the Create tab. The compact checkbox keeps persistent-consent details in +its tooltip. The native Maestro flow opens the shared form by deep link, enables +the preference there, and verifies persistence in settings after a relaunch. +The browser flow also holds a recording opt-out request to verify another privacy toggle cannot overwrite it while saving. Deleting the node's settings record also withdraws automatic publishing consent; the PDS-backed regression for that index deletion lives in `pkg/atproto/auto_publish_vod_test.go`. It also injects a deletion failure diff --git a/js/e2e-web/flows/auto-publish-vods.spec.ts b/js/e2e-web/flows/auto-publish-vods.spec.ts index d201d39d..15829e59 100644 --- a/js/e2e-web/flows/auto-publish-vods.spec.ts +++ b/js/e2e-web/flows/auto-publish-vods.spec.ts @@ -3,10 +3,10 @@ import { loginThroughPds } from "./login"; // Automatic VOD publishing is the autoPublishVods field of the account's // place.stream.server.settings record for this node, shared by Privacy & -// Security and the desktop live dashboard. The harness account has VOD beta +// Security and the stream form's Create tab. The harness account has VOD beta // access. It records no streams (no S3), so publishing itself is covered by // the Go tests in pkg/statedb and pkg/atproto. -// Mobile settings: .maestro/logged-in/auto-publish-vods.yaml. +// Native form and settings: .maestro/logged-in/auto-publish-vods.yaml. const HTTPS_URL = process.env.SERVER_HTTPS_URL; test.skip(!HTTPS_URL, "harness started without its HTTPS hostnames"); @@ -96,7 +96,7 @@ test("auto-publish-vods: recording opt-out survives another toggle", async ({ } }); -test("auto-publish-vods: dashboard and privacy share the saved preference", async ({ +test("auto-publish-vods: stream form and privacy share the saved preference", async ({ page, }) => { await loginThroughPds(page); @@ -105,6 +105,11 @@ test("auto-publish-vods: dashboard and privacy share the saved preference", asyn await expect(toggle).toBeVisible({ timeout: 30_000 }); await expect(toggle).toBeEnabled(); const initial = (await toggle.getAttribute("aria-checked")) === "true"; + const form = page.getByTestId("live-stream-form"); + await form.getByText("Metadata", { exact: true }).click(); + await expect(toggle).toBeHidden(); + await form.getByText("Create", { exact: true }).click(); + await expect(toggle).toBeVisible(); await setSetting(page, "live-auto-publish-vods", !initial); await page.goto(`${HTTPS_URL}/settings/privacy`); @@ -113,6 +118,11 @@ test("auto-publish-vods: dashboard and privacy share the saved preference", asyn String(!initial), { timeout: 30_000 }, ); + await page.goto(`${HTTPS_URL}/widgets/livestream`); + await expect(toggle).toHaveAttribute("aria-checked", String(!initial), { + timeout: 30_000, + }); + await page.goto(`${HTTPS_URL}/settings/privacy`); await setSetting(page, "settings-auto-publish-vods", initial); await page.goto(`${HTTPS_URL}/live`); await expect(toggle).toHaveAttribute("aria-checked", String(initial), { @@ -131,6 +141,11 @@ test("auto-publish-vods: dashboard and privacy share the saved preference", asyn ), page.reload(), ]); - await expect(page.getByTestId("live-stream-destinations")).toBeVisible(); + await expect( + page.getByRole("checkbox", { + name: "End livestream automatically", + exact: true, + }), + ).toBeVisible(); await expect(toggle).toBeHidden(); }); diff --git a/js/i18n/locales/en-US/settings.ftl b/js/i18n/locales/en-US/settings.ftl index 82266707..a3d1e299 100644 --- a/js/i18n/locales/en-US/settings.ftl +++ b/js/i18n/locales/en-US/settings.ftl @@ -46,8 +46,8 @@ livestream-recording-title = Record my livestreams into VODs on { $host } livestream-recording-description = When enabled, your livestreams are saved so you can finalize and publish them as videos auto-publish-vods-title = Publish my livestreams as VODs automatically auto-publish-vods-description = When a recorded livestream ends, { $host } publishes it as a video right away, without waiting for you to finalize it -auto-publish-vods-dashboard-label = Automatically publish this stream as a VOD after it ends -auto-publish-vods-dashboard-description = Also applies to future recorded streams until you turn it off. +auto-publish-vods-stream-label = Publish VOD automatically +auto-publish-vods-stream-description = Publish recorded streams as VODs automatically. Applies to future streams too, until you turn it off. ## Key Management manage-keys = Manage Keys diff --git a/js/i18n/public/locales/en-US/settings.json b/js/i18n/public/locales/en-US/settings.json index 7104140b..b372696f 100644 --- a/js/i18n/public/locales/en-US/settings.json +++ b/js/i18n/public/locales/en-US/settings.json @@ -34,8 +34,8 @@ "livestream-recording-description": "When enabled, your livestreams are saved so you can finalize and publish them as videos", "auto-publish-vods-title": "Publish my livestreams as VODs automatically", "auto-publish-vods-description": "When a recorded livestream ends, {$host} publishes it as a video right away, without waiting for you to finalize it", - "auto-publish-vods-dashboard-label": "Automatically publish this stream as a VOD after it ends", - "auto-publish-vods-dashboard-description": "Also applies to future recorded streams until you turn it off.", + "auto-publish-vods-stream-label": "Publish VOD automatically", + "auto-publish-vods-stream-description": "Publish recorded streams as VODs automatically. Applies to future streams too, until you turn it off.", "manage-keys": "Manage Keys", "settings-title": "Settings", "error": "Error", diff --git a/js/web/public/locales/en-US/settings.json b/js/web/public/locales/en-US/settings.json index 7104140b..b372696f 100644 --- a/js/web/public/locales/en-US/settings.json +++ b/js/web/public/locales/en-US/settings.json @@ -34,8 +34,8 @@ "livestream-recording-description": "When enabled, your livestreams are saved so you can finalize and publish them as videos", "auto-publish-vods-title": "Publish my livestreams as VODs automatically", "auto-publish-vods-description": "When a recorded livestream ends, {$host} publishes it as a video right away, without waiting for you to finalize it", - "auto-publish-vods-dashboard-label": "Automatically publish this stream as a VOD after it ends", - "auto-publish-vods-dashboard-description": "Also applies to future recorded streams until you turn it off.", + "auto-publish-vods-stream-label": "Publish VOD automatically", + "auto-publish-vods-stream-description": "Publish recorded streams as VODs automatically. Applies to future streams too, until you turn it off.", "manage-keys": "Manage Keys", "settings-title": "Settings", "error": "Error", -- 2.51.2