From 1312fe3361b41b61ee3642f897454e2c93f75427 Mon Sep 17 00:00:00 2001
From: webadderall <131426131+webadderall@users.noreply.github.com>
Date: Wed, 2 Sep 2026 18:58:44 +1000
Subject: [PATCH] Simplify export choices and show useful messages while
rendering
---
.../announcements/AnnouncementDialog.tsx | 3 +-
.../video-editor/ExportSettingsMenu.tsx | 65 --------
.../video-editor/editorPreferences.test.ts | 6 +
.../export/useEditorExportController.ts | 15 +-
.../export/useExportMessages.test.ts | 23 +++
.../video-editor/export/useExportMessages.ts | 141 ++++++++++++++++++
.../video-editor/layout/EditorExportMenu.tsx | 21 +--
.../video-editor/layout/EditorHeader.tsx | 6 +-
.../video-editor/layout/EditorShell.tsx | 4 +-
.../video-editor/projectPersistence.ts | 8 +-
src/i18n/locales/de/editor.json | 5 +
src/i18n/locales/en/editor.json | 5 +
src/i18n/locales/es/editor.json | 5 +
src/i18n/locales/fr/editor.json | 5 +
src/i18n/locales/it/editor.json | 5 +
src/i18n/locales/ko/editor.json | 5 +
src/i18n/locales/nl/editor.json | 5 +
src/i18n/locales/pt-BR/editor.json | 5 +
src/i18n/locales/ru/editor.json | 5 +
src/i18n/locales/zh-CN/editor.json | 5 +
src/i18n/locales/zh-TW/editor.json | 5 +
src/lib/announcements.test.ts | 27 ++++
src/lib/announcements.ts | 9 +-
23 files changed, 279 insertions(+), 104 deletions(-)
create mode 100644 src/components/video-editor/export/useExportMessages.test.ts
create mode 100644 src/components/video-editor/export/useExportMessages.ts
diff --git a/src/components/announcements/AnnouncementDialog.tsx b/src/components/announcements/AnnouncementDialog.tsx
index 2496c6c7..a9bc0fcf 100644
--- a/src/components/announcements/AnnouncementDialog.tsx
+++ b/src/components/announcements/AnnouncementDialog.tsx
@@ -102,7 +102,8 @@ export function AnnouncementDialog({ audience }: { audience: AnnouncementAudienc
}).filter(
(announcement) =>
announcement.presentation !== "notification" &&
- announcement.presentation !== "banner",
+ announcement.presentation !== "banner" &&
+ announcement.presentation !== "export",
);
setPopupAspectRatio(
parsedRemoteFeed.settings.aspectRatio ??
diff --git a/src/components/video-editor/ExportSettingsMenu.tsx b/src/components/video-editor/ExportSettingsMenu.tsx
index dadf7e9c..c2c469fe 100644
--- a/src/components/video-editor/ExportSettingsMenu.tsx
+++ b/src/components/video-editor/ExportSettingsMenu.tsx
@@ -25,7 +25,6 @@ interface ExportSettingsMenuProps {
mp4FrameRate: ExportMp4FrameRate;
onMp4FrameRateChange?: (frameRate: ExportMp4FrameRate) => void;
exportPipelineModel?: ExportPipelineModel;
- onExportPipelineModelChange?: (pipelineModel: ExportPipelineModel) => void;
experimentalNvidiaCudaExport?: boolean;
onExperimentalNvidiaCudaExportChange?: (enabled: boolean) => void;
nvidiaCudaExportAvailable?: boolean;
@@ -54,7 +53,6 @@ export function ExportSettingsMenu({
mp4FrameRate,
onMp4FrameRateChange,
exportPipelineModel = "modern",
- onExportPipelineModelChange,
experimentalNvidiaCudaExport = false,
onExperimentalNvidiaCudaExportChange,
nvidiaCudaExportAvailable = false,
@@ -281,69 +279,6 @@ export function ExportSettingsMenu({
);
})}
-
-
- {tSettings("export.pipelineTitle", "Pipeline")}
-
-
-
- {(
- [
- {
- value: "legacy",
- label: tSettings("export.pipeline.legacy", "Legacy"),
- },
- {
- value: "modern",
- label: tSettings("export.pipeline.modern", "Lightning (Beta)"),
- },
- ] as const
- ).map((option) => {
- const isActive = exportPipelineModel === option.value;
- return (
-
- );
- })}
-
-
- {isLegacyModel
- ? tSettings(
- "export.pipeline.legacyHint",
- "Legacy uses the current stable WebCodecs export path.",
- )
- : tSettings(
- "export.pipeline.lightningHint",
- "Lightning (Beta) automatically uses the fastest compatible backend and falls back when needed.",
- )}
-
{!isLegacyModel && nvidiaCudaExportAvailable ? (
diff --git a/src/components/video-editor/editorPreferences.test.ts b/src/components/video-editor/editorPreferences.test.ts
index 53e4bff2..497e5e49 100644
--- a/src/components/video-editor/editorPreferences.test.ts
+++ b/src/components/video-editor/editorPreferences.test.ts
@@ -97,6 +97,12 @@ describe("editorPreferences", () => {
expect(DEFAULT_EDITOR_PREFERENCES.exportPipelineModel).toBe("modern");
});
+ it("migrates hidden legacy pipeline preferences to Lightning", () => {
+ expect(normalizeEditorPreferences({ exportPipelineModel: "legacy" })).toMatchObject({
+ exportPipelineModel: "modern",
+ });
+ });
+
it("bakes in the stronger split motion blur defaults", () => {
expect(DEFAULT_EDITOR_PREFERENCES.zoomMotionBlurTuning).toMatchObject({
panVelocityThreshold: 0,
diff --git a/src/components/video-editor/export/useEditorExportController.ts b/src/components/video-editor/export/useEditorExportController.ts
index 60911c7d..ec6aa3f7 100644
--- a/src/components/video-editor/export/useEditorExportController.ts
+++ b/src/components/video-editor/export/useEditorExportController.ts
@@ -1,15 +1,14 @@
import type { RefObject } from "react";
-import { useCallback } from "react";
import type { useI18n } from "@/contexts/I18nContext";
import type { useVideoEditorAudio } from "../audio/useVideoEditorAudio";
import type { getSmokeExportConfig } from "../smokeExportConfig";
import type { useAppearanceState } from "../state/useAppearanceState";
import type { useTimelineState } from "../state/useTimelineState";
-import { openExternalLink, RECORDLY_ISSUES_URL } from "../TutorialHelp";
import type { CursorTelemetryPoint, SpeedRegion, ZoomRegion } from "../types";
import type { VideoPlaybackRef } from "../VideoPlayback";
import { useExportDialogActions } from "./useExportDialogActions";
import type { useExportDimensions } from "./useExportDimensions";
+import { useExportMessages } from "./useExportMessages";
import { useExportRunner } from "./useExportRunner";
import type { useExportSession } from "./useExportSession";
import type { useExportSettings } from "./useExportSettings";
@@ -91,12 +90,10 @@ export function useEditorExportController(input: Input) {
session: input.session,
settings: input.settings,
});
- const openLightningIssues = useCallback(async () => {
- await openExternalLink(
- RECORDLY_ISSUES_URL,
- input.t("editor.feedback.openFailed", "Failed to open link."),
- );
- }, [input.t]);
+ const exportMessage = useExportMessages({
+ t: input.t,
+ active: status.isLightningExportInProgress,
+ });
- return { dialogActions, status, openLightningIssues };
+ return { dialogActions, status, exportMessage };
}
diff --git a/src/components/video-editor/export/useExportMessages.test.ts b/src/components/video-editor/export/useExportMessages.test.ts
new file mode 100644
index 00000000..2d815b19
--- /dev/null
+++ b/src/components/video-editor/export/useExportMessages.test.ts
@@ -0,0 +1,23 @@
+import { describe, expect, it } from "vitest";
+import { buildExportMessageStream, type ExportMessage } from "./useExportMessages";
+
+function message(id: string): ExportMessage {
+ return { id, text: id, durationSeconds: 6 };
+}
+
+describe("buildExportMessageStream", () => {
+ it("alternates built-in tips with streamed announcements", () => {
+ expect(
+ buildExportMessageStream(
+ [message("tip-1"), message("tip-2"), message("tip-3")],
+ [message("announcement-1"), message("announcement-2")],
+ ).map(({ id }) => id),
+ ).toEqual(["tip-1", "announcement-1", "tip-2", "announcement-2", "tip-3"]);
+ });
+
+ it("uses only built-in tips when the announcement feed is empty", () => {
+ expect(
+ buildExportMessageStream([message("tip-1"), message("tip-2")], []).map(({ id }) => id),
+ ).toEqual(["tip-1", "tip-2"]);
+ });
+});
diff --git a/src/components/video-editor/export/useExportMessages.ts b/src/components/video-editor/export/useExportMessages.ts
new file mode 100644
index 00000000..f89d8152
--- /dev/null
+++ b/src/components/video-editor/export/useExportMessages.ts
@@ -0,0 +1,141 @@
+import { useEffect, useMemo, useRef, useState } from "react";
+import { BUNDLED_ANNOUNCEMENT_FEED } from "@/content/announcements";
+import type { useI18n } from "@/contexts/I18nContext";
+import {
+ readAnnouncementImpressionCounts,
+ readDismissedAnnouncementIds,
+ recordAnnouncementImpression,
+} from "@/lib/announcementState";
+import type { Announcement } from "@/lib/announcements";
+import { parseAnnouncementFeed, selectAnnouncements } from "@/lib/announcements";
+
+const DEFAULT_MESSAGE_DURATION_SECONDS = 6;
+const MAX_EXPORT_MESSAGE_LENGTH = 220;
+
+export type ExportMessage = {
+ id: string;
+ text: string;
+ durationSeconds: number;
+ announcementId?: string;
+};
+
+function compactMessage(value: string): string {
+ const compacted = value.replace(/\s+/g, " ").trim();
+ if (compacted.length <= MAX_EXPORT_MESSAGE_LENGTH) return compacted;
+ return `${compacted.slice(0, MAX_EXPORT_MESSAGE_LENGTH - 1).trimEnd()}…`;
+}
+
+export function buildExportMessageStream(
+ tips: ExportMessage[],
+ announcements: ExportMessage[],
+): ExportMessage[] {
+ const stream: ExportMessage[] = [];
+ const length = Math.max(tips.length, announcements.length);
+ for (let index = 0; index < length; index += 1) {
+ if (tips[index]) stream.push(tips[index]);
+ if (announcements[index]) stream.push(announcements[index]);
+ }
+ return stream;
+}
+
+function toExportMessage(announcement: Announcement): ExportMessage {
+ return {
+ id: `announcement:${announcement.id}`,
+ announcementId: announcement.id,
+ text: compactMessage(`${announcement.title} — ${announcement.body}`),
+ durationSeconds: announcement.displayDurationSeconds ?? DEFAULT_MESSAGE_DURATION_SECONDS,
+ };
+}
+
+export function useExportMessages({
+ t,
+ active,
+}: {
+ t: ReturnType
["t"];
+ active: boolean;
+}): string | null {
+ const [announcementMessages, setAnnouncementMessages] = useState([]);
+ const [currentIndex, setCurrentIndex] = useState(0);
+ const countedThisSessionRef = useRef(new Set());
+ const tips = useMemo(
+ () => [
+ {
+ id: "tip:auto-zooms",
+ text: t(
+ "editor.exportTips.autoZooms",
+ "Tip: Turn off auto-applied zooms in settings",
+ ),
+ durationSeconds: DEFAULT_MESSAGE_DURATION_SECONDS,
+ },
+ {
+ id: "tip:experimental-builds",
+ text: t(
+ "editor.exportTips.experimentalBuilds",
+ "Tip: Try experimental builds by turning on access in settings",
+ ),
+ durationSeconds: DEFAULT_MESSAGE_DURATION_SECONDS,
+ },
+ {
+ id: "tip:cursor-appearance",
+ text: t(
+ "editor.exportTips.cursorAppearance",
+ "Tip: You can customise your cursor appearance",
+ ),
+ durationSeconds: DEFAULT_MESSAGE_DURATION_SECONDS,
+ },
+ ],
+ [t],
+ );
+ const messages = useMemo(
+ () => buildExportMessageStream(tips, announcementMessages),
+ [tips, announcementMessages],
+ );
+ const current = messages[currentIndex % messages.length];
+
+ useEffect(() => {
+ let cancelled = false;
+ const loadExportAnnouncements = async () => {
+ const dismissedIds = new Set(readDismissedAnnouncementIds());
+ const impressionCounts = readAnnouncementImpressionCounts();
+ const [appVersion, remoteFeed] = await Promise.all([
+ window.electronAPI.getAppVersion().catch(() => "0.0.0"),
+ window.electronAPI.getAnnouncements().catch(() => null),
+ ]);
+ if (cancelled) return;
+ setAnnouncementMessages(
+ selectAnnouncements({
+ bundled: BUNDLED_ANNOUNCEMENT_FEED.announcements,
+ remote: parseAnnouncementFeed(remoteFeed).announcements,
+ dismissedIds,
+ impressionCounts,
+ appVersion,
+ audience: "editor",
+ })
+ .filter((announcement) => announcement.presentation === "export")
+ .map(toExportMessage),
+ );
+ };
+ void loadExportAnnouncements();
+ return () => {
+ cancelled = true;
+ };
+ }, []);
+
+ useEffect(() => {
+ if (!active || messages.length < 2 || !current) return;
+ const timeout = window.setTimeout(
+ () => setCurrentIndex((index) => (index + 1) % messages.length),
+ current.durationSeconds * 1_000,
+ );
+ return () => window.clearTimeout(timeout);
+ }, [active, current, messages.length]);
+
+ useEffect(() => {
+ const announcementId = active ? current?.announcementId : undefined;
+ if (!announcementId || countedThisSessionRef.current.has(announcementId)) return;
+ countedThisSessionRef.current.add(announcementId);
+ recordAnnouncementImpression(announcementId);
+ }, [active, current]);
+
+ return active ? (current?.text ?? null) : null;
+}
diff --git a/src/components/video-editor/layout/EditorExportMenu.tsx b/src/components/video-editor/layout/EditorExportMenu.tsx
index 64b247fd..b39a8521 100644
--- a/src/components/video-editor/layout/EditorExportMenu.tsx
+++ b/src/components/video-editor/layout/EditorExportMenu.tsx
@@ -28,7 +28,7 @@ type Props = {
handleRetrySaveExport: () => void;
handleStartExportFromDropdown: () => void;
revealExportedFile: () => void;
- openLightningIssues: () => void;
+ exportMessage: string | null;
};
export function EditorExportMenu(props: Props) {
@@ -48,7 +48,7 @@ export function EditorExportMenu(props: Props) {
handleRetrySaveExport,
handleStartExportFromDropdown,
revealExportedFile,
- openLightningIssues,
+ exportMessage,
} = props;
const {
exportQuality,
@@ -56,7 +56,6 @@ export function EditorExportMenu(props: Props) {
exportEncodingMode,
setExportEncodingMode,
exportPipelineModel,
- setExportPipelineModel,
mp4FrameRate,
setMp4FrameRate,
exportFormat,
@@ -123,18 +122,9 @@ export function EditorExportMenu(props: Props) {
{t("editor.exportStatus.renderingFile", "Rendering your file.")}
- {isLightningExportInProgress ? (
-
- PLEASE{" "}
-
- with Lightning export{" "}
- {"\u{1F64F}"}
+ {isLightningExportInProgress && exportMessage ? (
+
+ {exportMessage}
) : null}
{isLegacyExportInProgress ? (
@@ -269,7 +259,6 @@ export function EditorExportMenu(props: Props) {
mp4FrameRate={mp4FrameRate}
onMp4FrameRateChange={setMp4FrameRate}
exportPipelineModel={exportPipelineModel}
- onExportPipelineModelChange={setExportPipelineModel}
experimentalNvidiaCudaExport={
experimentalNvidiaCudaExport && nvidiaCudaExportAvailable
}
diff --git a/src/components/video-editor/layout/EditorHeader.tsx b/src/components/video-editor/layout/EditorHeader.tsx
index 76fd0cd6..927830d8 100644
--- a/src/components/video-editor/layout/EditorHeader.tsx
+++ b/src/components/video-editor/layout/EditorHeader.tsx
@@ -46,7 +46,7 @@ type Props = {
handleRetrySaveExport: () => void;
handleStartExportFromDropdown: () => void;
revealExportedFile: () => void;
- openLightningIssues: () => void;
+ exportMessage: string | null;
};
export function EditorHeader(props: Props) {
@@ -80,7 +80,7 @@ export function EditorHeader(props: Props) {
handleRetrySaveExport,
handleStartExportFromDropdown,
revealExportedFile,
- openLightningIssues,
+ exportMessage,
} = props;
const {
isEditingProjectName,
@@ -219,7 +219,7 @@ export function EditorHeader(props: Props) {
handleRetrySaveExport={handleRetrySaveExport}
handleStartExportFromDropdown={handleStartExportFromDropdown}
revealExportedFile={revealExportedFile}
- openLightningIssues={openLightningIssues}
+ exportMessage={exportMessage}
/>
diff --git a/src/components/video-editor/layout/EditorShell.tsx b/src/components/video-editor/layout/EditorShell.tsx
index b2defeb3..e8592828 100644
--- a/src/components/video-editor/layout/EditorShell.tsx
+++ b/src/components/video-editor/layout/EditorShell.tsx
@@ -89,7 +89,7 @@ export function EditorShell(props: Props) {
handleSelectAnnotation,
handleAutoSuggestZoomsConsumed,
} = editing;
- const { dialogActions, status: exportStatus, openLightningIssues } = exportController;
+ const { dialogActions, status: exportStatus, exportMessage } = exportController;
const editorDialogs = (
diff --git a/src/components/video-editor/projectPersistence.ts b/src/components/video-editor/projectPersistence.ts
index 8e71bde7..1a098f40 100644
--- a/src/components/video-editor/projectPersistence.ts
+++ b/src/components/video-editor/projectPersistence.ts
@@ -197,11 +197,9 @@ export function normalizeExportBackendPreference(value: unknown): ExportBackendP
return "auto";
}
-export function normalizeExportPipelineModel(value: unknown): ExportPipelineModel {
- if (value === "modern" || value === "legacy") {
- return value;
- }
-
+export function normalizeExportPipelineModel(_value: unknown): ExportPipelineModel {
+ // Legacy remains available to internal smoke/export routing, but persisted
+ // user selections migrate to the only pipeline exposed by the editor UI.
return "modern";
}
diff --git a/src/i18n/locales/de/editor.json b/src/i18n/locales/de/editor.json
index 4fe144c2..294c2ce5 100644
--- a/src/i18n/locales/de/editor.json
+++ b/src/i18n/locales/de/editor.json
@@ -117,6 +117,11 @@
"discardChanges": "Änderungen verwerfen",
"saveProject": "Projekt speichern"
},
+ "exportTips": {
+ "autoZooms": "Tipp: Automatisch angewendete Zooms können in den Einstellungen deaktiviert werden",
+ "experimentalBuilds": "Tipp: Aktiviere den Zugriff auf experimentelle Builds in den Einstellungen",
+ "cursorAppearance": "Tipp: Du kannst das Aussehen deines Cursors anpassen"
+ },
"account": { "title": "Konto", "comingSoon": "Konto demnächst verfügbar" },
"nativeCaptureUnavailable": {
"title": "Es ist nichts kaputt, aber wir können kein animiertes Cursor-Overlay rendern.",
diff --git a/src/i18n/locales/en/editor.json b/src/i18n/locales/en/editor.json
index ad5e2902..099393ba 100644
--- a/src/i18n/locales/en/editor.json
+++ b/src/i18n/locales/en/editor.json
@@ -118,6 +118,11 @@
"discardChanges": "Discard changes",
"saveProject": "Save project"
},
+ "exportTips": {
+ "autoZooms": "Tip: Turn off auto-applied zooms in settings",
+ "experimentalBuilds": "Tip: Try experimental builds by turning on access in settings",
+ "cursorAppearance": "Tip: You can customise your cursor appearance"
+ },
"account": { "title": "Account", "comingSoon": "Account coming soon" },
"nativeCaptureUnavailable": {
"title": "Nothing’s broken, but we won’t be able to render an animated cursor overlay.",
diff --git a/src/i18n/locales/es/editor.json b/src/i18n/locales/es/editor.json
index a0ba68ce..c5da834b 100644
--- a/src/i18n/locales/es/editor.json
+++ b/src/i18n/locales/es/editor.json
@@ -118,6 +118,11 @@
"discardChanges": "Descartar cambios",
"saveProject": "Guardar proyecto"
},
+ "exportTips": {
+ "autoZooms": "Consejo: Desactiva los zooms automáticos en la configuración",
+ "experimentalBuilds": "Consejo: Activa el acceso a versiones experimentales en la configuración",
+ "cursorAppearance": "Consejo: Puedes personalizar la apariencia del cursor"
+ },
"account": { "title": "Cuenta", "comingSoon": "Cuenta próximamente" },
"nativeCaptureUnavailable": {
"title": "Nada está roto, pero no podremos renderizar una superposición de cursor animada.",
diff --git a/src/i18n/locales/fr/editor.json b/src/i18n/locales/fr/editor.json
index 4ab0b54b..08704aaa 100644
--- a/src/i18n/locales/fr/editor.json
+++ b/src/i18n/locales/fr/editor.json
@@ -118,6 +118,11 @@
"discardChanges": "Ignorer les modifications",
"saveProject": "Enregistrer le projet"
},
+ "exportTips": {
+ "autoZooms": "Astuce : Désactivez les zooms automatiques dans les paramètres",
+ "experimentalBuilds": "Astuce : Activez l’accès aux versions expérimentales dans les paramètres",
+ "cursorAppearance": "Astuce : Vous pouvez personnaliser l’apparence du curseur"
+ },
"account": { "title": "Compte", "comingSoon": "Compte bientôt disponible" },
"nativeCaptureUnavailable": {
"title": "Rien n'est cassé, mais nous ne pourrons pas afficher une superposition animée du curseur.",
diff --git a/src/i18n/locales/it/editor.json b/src/i18n/locales/it/editor.json
index 4448951a..0576b03c 100644
--- a/src/i18n/locales/it/editor.json
+++ b/src/i18n/locales/it/editor.json
@@ -118,6 +118,11 @@
"discardChanges": "Ignora modifiche",
"saveProject": "Salva progetto"
},
+ "exportTips": {
+ "autoZooms": "Suggerimento: Disattiva gli zoom applicati automaticamente nelle impostazioni",
+ "experimentalBuilds": "Suggerimento: Attiva l’accesso alle build sperimentali nelle impostazioni",
+ "cursorAppearance": "Suggerimento: Puoi personalizzare l’aspetto del cursore"
+ },
"account": { "title": "Account", "comingSoon": "Account in arrivo" },
"nativeCaptureUnavailable": {
"title": "Niente è rotto, ma non sarà possibile renderizzare un overlay del cursore animato.",
diff --git a/src/i18n/locales/ko/editor.json b/src/i18n/locales/ko/editor.json
index 3a974ea2..ddc411b9 100644
--- a/src/i18n/locales/ko/editor.json
+++ b/src/i18n/locales/ko/editor.json
@@ -119,6 +119,11 @@
"discardChanges": "변경 사항 버리기",
"saveProject": "프로젝트 저장"
},
+ "exportTips": {
+ "autoZooms": "팁: 설정에서 자동 적용 확대를 끌 수 있습니다",
+ "experimentalBuilds": "팁: 설정에서 실험적 빌드 액세스를 켜 보세요",
+ "cursorAppearance": "팁: 커서 모양을 사용자 지정할 수 있습니다"
+ },
"account": { "title": "계정", "comingSoon": "계정 기능 준비 중" },
"nativeCaptureUnavailable": {
"title": "문제가 생긴 것은 아니지만, 애니메이션 커서 오버레이를 렌더링할 수 없습니다.",
diff --git a/src/i18n/locales/nl/editor.json b/src/i18n/locales/nl/editor.json
index 00cec3d1..9d9376c7 100644
--- a/src/i18n/locales/nl/editor.json
+++ b/src/i18n/locales/nl/editor.json
@@ -119,6 +119,11 @@
"discardChanges": "Wijzigingen negeren",
"saveProject": "Project opslaan"
},
+ "exportTips": {
+ "autoZooms": "Tip: Schakel automatisch toegepaste zooms uit in de instellingen",
+ "experimentalBuilds": "Tip: Schakel toegang tot experimentele builds in via de instellingen",
+ "cursorAppearance": "Tip: Je kunt het uiterlijk van je cursor aanpassen"
+ },
"account": { "title": "Account", "comingSoon": "Account binnenkort beschikbaar" },
"nativeCaptureUnavailable": {
"title": "Er is niets kapot, maar we kunnen geen geanimeerde cursor-overlay renderen.",
diff --git a/src/i18n/locales/pt-BR/editor.json b/src/i18n/locales/pt-BR/editor.json
index 216ca338..1a376631 100644
--- a/src/i18n/locales/pt-BR/editor.json
+++ b/src/i18n/locales/pt-BR/editor.json
@@ -118,6 +118,11 @@
"discardChanges": "Descartar alterações",
"saveProject": "Salvar projeto"
},
+ "exportTips": {
+ "autoZooms": "Dica: Desative os zooms aplicados automaticamente nas configurações",
+ "experimentalBuilds": "Dica: Ative o acesso a versões experimentais nas configurações",
+ "cursorAppearance": "Dica: Você pode personalizar a aparência do cursor"
+ },
"account": { "title": "Conta", "comingSoon": "Conta em breve" },
"nativeCaptureUnavailable": {
"title": "Nada está quebrado, mas não poderemos renderizar uma sobreposição animada do cursor.",
diff --git a/src/i18n/locales/ru/editor.json b/src/i18n/locales/ru/editor.json
index 847d0a93..56c62e1c 100644
--- a/src/i18n/locales/ru/editor.json
+++ b/src/i18n/locales/ru/editor.json
@@ -118,6 +118,11 @@
"discardChanges": "Отменить изменения",
"saveProject": "Сохранить проект"
},
+ "exportTips": {
+ "autoZooms": "Совет: Автоматические приближения можно отключить в настройках",
+ "experimentalBuilds": "Совет: Включите доступ к экспериментальным сборкам в настройках",
+ "cursorAppearance": "Совет: Внешний вид курсора можно настроить"
+ },
"account": { "title": "Учётная запись", "comingSoon": "Учётная запись скоро появится" },
"nativeCaptureUnavailable": {
"title": "Всё в порядке, но мы не можем отобразить анимированное наложение курсора.",
diff --git a/src/i18n/locales/zh-CN/editor.json b/src/i18n/locales/zh-CN/editor.json
index 79487495..bb799ed1 100644
--- a/src/i18n/locales/zh-CN/editor.json
+++ b/src/i18n/locales/zh-CN/editor.json
@@ -118,6 +118,11 @@
"discardChanges": "放弃更改",
"saveProject": "保存项目"
},
+ "exportTips": {
+ "autoZooms": "提示:可在设置中关闭自动应用的缩放",
+ "experimentalBuilds": "提示:可在设置中开启实验版本访问权限",
+ "cursorAppearance": "提示:你可以自定义光标外观"
+ },
"account": { "title": "账户", "comingSoon": "账户功能即将推出" },
"nativeCaptureUnavailable": {
"title": "没有出错,但我们无法渲染动画光标叠加层。",
diff --git a/src/i18n/locales/zh-TW/editor.json b/src/i18n/locales/zh-TW/editor.json
index 9d49ee2f..0332aed1 100644
--- a/src/i18n/locales/zh-TW/editor.json
+++ b/src/i18n/locales/zh-TW/editor.json
@@ -118,6 +118,11 @@
"discardChanges": "捨棄變更",
"saveProject": "儲存專案"
},
+ "exportTips": {
+ "autoZooms": "提示:可在設定中關閉自動套用的縮放",
+ "experimentalBuilds": "提示:可在設定中開啟實驗版本存取權",
+ "cursorAppearance": "提示:你可以自訂游標外觀"
+ },
"account": { "title": "帳號", "comingSoon": "帳號功能即將推出" },
"nativeCaptureUnavailable": {
"title": "沒有出錯,但我們無法轉譯動畫游標覆蓋層。",
diff --git a/src/lib/announcements.test.ts b/src/lib/announcements.test.ts
index a616c924..573158dd 100644
--- a/src/lib/announcements.test.ts
+++ b/src/lib/announcements.test.ts
@@ -116,6 +116,33 @@ describe("parseAnnouncementFeed", () => {
]);
});
+ it("accepts text-only messages for the export status stream", () => {
+ const feed = parseAnnouncementFeed({
+ announcements: [
+ {
+ id: "export-tip-1",
+ title: "Did you know?",
+ body: "Cursor styles can be changed in the editor.",
+ presentation: "export",
+ media: { type: "image", url: "https://example.com/ignored.jpg" },
+ displayDurationSeconds: 8,
+ },
+ ],
+ });
+
+ expect(feed.announcements).toEqual([
+ {
+ id: "export-tip-1",
+ title: "Did you know?",
+ body: "Cursor styles can be changed in the editor.",
+ presentation: "export",
+ audience: "all",
+ priority: 0,
+ displayDurationSeconds: 8,
+ },
+ ]);
+ });
+
it("accepts an action that opens a safe editor section", () => {
const feed = parseAnnouncementFeed({
announcements: [
diff --git a/src/lib/announcements.ts b/src/lib/announcements.ts
index 9a65c9f6..4509ce14 100644
--- a/src/lib/announcements.ts
+++ b/src/lib/announcements.ts
@@ -31,7 +31,7 @@ export interface Announcement {
id: string;
title: string;
body: string;
- presentation?: "popup" | "notification" | "banner";
+ presentation?: "popup" | "notification" | "banner" | "export";
audience: AnnouncementAudience;
priority: number;
mediaMode?: "banner" | "cover";
@@ -203,10 +203,13 @@ function parseAnnouncement(value: unknown): Announcement | undefined {
const audience = value.audience === "editor" ? "editor" : "all";
const presentation =
- value.presentation === "notification" || value.presentation === "banner"
+ value.presentation === "notification" ||
+ value.presentation === "banner" ||
+ value.presentation === "export"
? value.presentation
: undefined;
- const isTextOnlyPresentation = presentation === "notification" || presentation === "banner";
+ const isTextOnlyPresentation =
+ presentation === "notification" || presentation === "banner" || presentation === "export";
const priority =
typeof value.priority === "number" && Number.isFinite(value.priority)
? Math.max(-100, Math.min(100, value.priority))