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",