Fix Recordly 2.0 part 5 follow-up review findings

Preserve existing PR history and integrate validated current-cycle fixes.
This commit is contained in:
webadderall committed 2026-11-08 13:31:04 +11:00
commit ee287a687f
6 files changed
+145 -62

No files matched your search

+1 -1
View File
@@ -102,7 +102,7 @@ export function AccountSettings({
submittedForm.current = event.currentTarget;
void run("email", async () => {
const updated = await requestAccountEmailChange(email);
return updated.email === email.trim()
return updated.email?.toLowerCase() === email.trim().toLowerCase()
? t("settings.account.emailUpdated", "Email address updated.")
: t(
"settings.account.confirmInboxes",
@@ -17,71 +17,85 @@ export function RecordingPermissionsSettings({ onReview }: { onReview?: () => vo
const { t } = useI18n();
const mounted = useRef(false);
const pending = useRef(false);
const refreshQueued = useRef(false);
const [snapshot, setSnapshot] = useState<PermissionSnapshot>();
const [busy, setBusy] = useState(false);
const [opening, setOpening] = useState<Permission>();
const [error, setError] = useState<string>();
const [actionError, setActionError] = useState<{ permission: Permission; message: string }>();
const refresh = useCallback(async () => {
if (pending.current) return;
if (!mounted.current) return;
if (pending.current) {
refreshQueued.current = true;
return;
}
pending.current = true;
setBusy(true);
try {
const mac = (await window.electronAPI.getPlatform()) === "darwin";
const values: PermissionSnapshot["values"] = {};
let failed = false;
const results = await Promise.allSettled([
mac
? window.electronAPI.getScreenRecordingPermissionStatus()
: Promise.resolve(null),
mac ? window.electronAPI.getAccessibilityPermissionStatus() : Promise.resolve(null),
typeof window.electronAPI.getMediaPermissionStatus === "function"
? window.electronAPI.getMediaPermissionStatus()
: Promise.resolve(null),
]);
const [screen, accessibility, media] = results;
if (mac) {
if (screen.status === "fulfilled" && screen.value?.success)
values.screen = normalize(screen.value.status);
else {
values.screen = "unknown";
failed = true;
do {
refreshQueued.current = false;
try {
const mac = (await window.electronAPI.getPlatform()) === "darwin";
const values: PermissionSnapshot["values"] = {};
let failed = false;
const results = await Promise.allSettled([
mac
? window.electronAPI.getScreenRecordingPermissionStatus()
: Promise.resolve(null),
mac
? window.electronAPI.getAccessibilityPermissionStatus()
: Promise.resolve(null),
typeof window.electronAPI.getMediaPermissionStatus === "function"
? window.electronAPI.getMediaPermissionStatus()
: Promise.resolve(null),
]);
const [screen, accessibility, media] = results;
if (mac) {
if (screen.status === "fulfilled" && screen.value?.success)
values.screen = normalize(screen.value.status);
else {
values.screen = "unknown";
failed = true;
}
if (accessibility.status === "fulfilled" && accessibility.value?.success)
values.accessibility = accessibility.value.trusted
? "granted"
: "denied";
else {
values.accessibility = "unknown";
failed = true;
}
}
if (media.status === "fulfilled" && media.value?.success) {
values.microphone = normalize(media.value.microphone);
values.camera = normalize(media.value.camera);
} else {
values.microphone = "unknown";
values.camera = "unknown";
failed = true;
}
if (!mounted.current) return;
setSnapshot({ mac, values });
setError(
failed
? t(
"settings.permissions.checkFailed",
"Some permissions could not be checked. Try again.",
)
: undefined,
);
} catch {
if (mounted.current) {
setSnapshot(undefined);
setError(
t(
"settings.permissions.checkFailed",
"Some permissions could not be checked. Try again.",
),
);
}
}
if (accessibility.status === "fulfilled" && accessibility.value?.success)
values.accessibility = accessibility.value.trusted ? "granted" : "denied";
else {
values.accessibility = "unknown";
failed = true;
}
}
if (media.status === "fulfilled" && media.value?.success) {
values.microphone = normalize(media.value.microphone);
values.camera = normalize(media.value.camera);
} else {
values.microphone = "unknown";
values.camera = "unknown";
failed = true;
}
if (!mounted.current) return;
setSnapshot({ mac, values });
setError(
failed
? t(
"settings.permissions.checkFailed",
"Some permissions could not be checked. Try again.",
)
: undefined,
);
} catch {
if (mounted.current) {
setSnapshot(undefined);
setError(
t(
"settings.permissions.checkFailed",
"Some permissions could not be checked. Try again.",
),
);
}
} while (mounted.current && refreshQueued.current);
} finally {
pending.current = false;
if (mounted.current) setBusy(false);
@@ -144,8 +144,7 @@ export function GradientPicker({
{draft.colors.map((color, index) => (
<div key={index} className="flex items-center justify-between gap-2">
<span className="text-sm text-muted">
{te("color", "Color")}
{index + 1}
{te("colorNumber", "Color {{number}}", { number: index + 1 })}
</span>
<div className="flex items-center gap-1">
<ColorControl
@@ -1,4 +1,9 @@
import { describe, expect, it } from "vitest";
import type { ReactElement, ReactNode } from "react";
import { describe, expect, it, vi } from "vitest";
import { ColorControl } from "@/components/ui/color-picker";
import { type AppLocale, SUPPORTED_LOCALES } from "@/i18n/config";
import { translateForLocale } from "@/i18n/messages";
import { GradientPicker } from "./GradientPicker";
import {
createCustomGradient,
normalizeCustomGradients,
@@ -45,3 +50,57 @@ describe("custom gradient backgrounds", () => {
}
});
});
const gradientLocale = vi.hoisted(() => ({ value: "en" as AppLocale }));
vi.mock("react", async (original) => ({
...(await original<typeof import("react")>()),
useEffect: () => undefined,
useState: (initial: unknown) => [typeof initial === "function" ? initial() : initial, () => {}],
}));
vi.mock("@/contexts/I18nContext", () => ({
useScopedT:
(namespace: string) =>
(key: string, fallback?: string, values?: Record<string, string | number>) =>
translateForLocale(gradientLocale.value, `${namespace}.${key}`, fallback, values),
}));
function gradientNodes(node: ReactNode): ReactElement<Record<string, unknown>>[] {
if (Array.isArray(node)) return node.flatMap(gradientNodes);
if (!node || typeof node !== "object") return [];
const element = node as ReactElement<{ children?: ReactNode }>;
return [element, ...gradientNodes(element.props?.children)];
}
function gradientText(node: ReactNode): string {
if (Array.isArray(node)) return node.map(gradientText).join("");
if (node == null || typeof node === "boolean") return "";
if (typeof node !== "object") return String(node);
return gradientText((node as ReactElement<{ children?: ReactNode }>).props.children);
}
it.each(
SUPPORTED_LOCALES,
)("uses the localized numbered color label consistently in %s", (locale) => {
gradientLocale.value = locale;
const tree = GradientPicker({
value: "#ffffff",
presets: [],
onChange: vi.fn(),
tileClassName: () => "",
open: true,
});
const elements = gradientNodes(tree);
const rowLabels = elements.filter(
(node) => node.type === "span" && node.props.className === "text-sm text-muted",
);
const controls = elements.filter((node) => node.type === ColorControl);
expect(rowLabels).toHaveLength(2);
expect(controls).toHaveLength(2);
for (const [index, row] of rowLabels.entries()) {
const expected = translateForLocale(locale, "editing.colorNumber", undefined, {
number: index + 1,
});
expect(expected).not.toBe("editing.colorNumber");
expect(gradientText(row.props.children as ReactNode)).toBe(expected);
expect(controls[index].props.label).toBe(expected);
}
});
@@ -16,8 +16,13 @@ export function usePreviewMediaState(
const videoRef = useRef<HTMLVideoElement | null>(null);
const previewVideoSourceRef = useRef(new PreviewVideoSource());
const previewRenderLoopRef = useRef<ReturnType<typeof createPreviewRenderLoop> | null>(null);
const attachVideo = useCallback((video: HTMLVideoElement | null) => {
if (video !== videoRef.current) {
// Ref detachment precedes passive cleanup. Stop sampling before the source is destroyed.
previewRenderLoopRef.current?.dispose();
}
// VideoSource.destroy() clears the media URL, so only destroy it when
// React detaches the element, never during a layout effect cleanup.
previewVideoSourceRef.current.setVideo(video);
@@ -64,7 +69,6 @@ export function usePreviewMediaState(
const lastRenderedContentTimeRef = useRef<number | null>(null);
const previewRenderLoopRef = useRef<ReturnType<typeof createPreviewRenderLoop> | null>(null);
const requestPreviewRender = useCallback(() => {
previewRenderLoopRef.current?.requestRender();
}, []);
+9 -2
View File
@@ -12,6 +12,7 @@ export class RecordingFileWriter {
private queue = Promise.resolve();
private streamId: string | undefined;
private pendingBytes = 0;
private oversizedChunkBytes = 0;
private failure: string | undefined;
private cancelled = false;
private path: string | undefined;
@@ -28,8 +29,12 @@ export class RecordingFileWriter {
}
write(chunk: Blob) {
if (this.cancelled || this.failure || !chunk.size) return;
// A single delayed recorder Blob drains in slices; bound additional queued data.
if (this.pendingBytes > 0 && this.pendingBytes + chunk.size > 32 * 1024 * 1024) {
const isOversizedChunk = this.pendingBytes === 0 && chunk.size > 32 * 1024 * 1024;
// One delayed Blob may drain in slices alongside up to 32 MiB of later data.
if (
!isOversizedChunk &&
this.pendingBytes - this.oversizedChunkBytes + chunk.size > 32 * 1024 * 1024
) {
this.fail(
new Error(
"Recording storage cannot keep up. The partial recording will be preserved.",
@@ -37,6 +42,7 @@ export class RecordingFileWriter {
);
return;
}
if (isOversizedChunk) this.oversizedChunkBytes = chunk.size;
this.pendingBytes += chunk.size;
this.queue = this.queue
.then(async () => {
@@ -68,6 +74,7 @@ export class RecordingFileWriter {
.catch((error) => this.fail(error))
.finally(() => {
this.pendingBytes -= chunk.size;
if (isOversizedChunk) this.oversizedChunkBytes = 0;
});
}
cancel() {