fix(desktop): restore notification sounds (#2582)
Restore desktop notification sounds by default, add a persisted Play sound setting, and group notification permission and testing in a dedicated Notifications section.
This commit is contained in:
@@ -31,10 +31,11 @@ export function Alert({
|
||||
}: AlertProps) {
|
||||
const { theme } = useUnistyles();
|
||||
const accentColor = resolveAccentColor(variant, theme);
|
||||
const borderColor = variant === "success" ? theme.colors.border : accentColor;
|
||||
|
||||
const containerStyle = useMemo(
|
||||
() => [styles.container, accentColor ? { borderColor: accentColor } : null],
|
||||
[accentColor],
|
||||
() => [styles.container, borderColor ? { borderColor } : null],
|
||||
[borderColor],
|
||||
);
|
||||
|
||||
const titleStyle = useMemo(
|
||||
@@ -73,7 +74,7 @@ function resolveAccentColor(
|
||||
theme: ReturnType<typeof useUnistyles>["theme"],
|
||||
): string | null {
|
||||
if (variant === "info") return theme.colors.palette.blue[300];
|
||||
if (variant === "success") return theme.colors.palette.green[400];
|
||||
if (variant === "success") return theme.colors.statusSuccess;
|
||||
if (variant === "warning") return theme.colors.palette.amber[500];
|
||||
if (variant === "error") return theme.colors.destructive;
|
||||
return null;
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Text, View } from "react-native";
|
||||
import { withUnistyles } from "react-native-unistyles";
|
||||
import { RotateCw } from "lucide-react-native";
|
||||
import { Alert } from "@/components/ui/alert";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { DesktopPermissionRow } from "@/desktop/components/desktop-permission-row";
|
||||
import { useDesktopPermissions } from "@/desktop/permissions/use-desktop-permissions";
|
||||
import { useDesktopSettings } from "@/desktop/settings/desktop-settings";
|
||||
import { SettingsSection } from "@/screens/settings/settings-section";
|
||||
import { settingsStyles } from "@/styles/settings";
|
||||
|
||||
const ThemedRotateCw = withUnistyles(RotateCw, (theme) => ({
|
||||
size: theme.iconSize.md,
|
||||
color: theme.colors.foregroundMuted,
|
||||
}));
|
||||
|
||||
export function DesktopNotificationsSection() {
|
||||
const { t } = useTranslation();
|
||||
const { settings, isSaving, updateSettings } = useDesktopSettings();
|
||||
const {
|
||||
isDesktopApp,
|
||||
snapshot,
|
||||
isRefreshing,
|
||||
requestingPermission,
|
||||
testNotificationState,
|
||||
refreshPermissions,
|
||||
requestPermission,
|
||||
sendTestNotification,
|
||||
} = useDesktopPermissions();
|
||||
|
||||
const handleRefreshPress = useCallback(() => {
|
||||
void refreshPermissions();
|
||||
}, [refreshPermissions]);
|
||||
|
||||
const handleRequestNotifications = useCallback(() => {
|
||||
void requestPermission("notifications");
|
||||
}, [requestPermission]);
|
||||
|
||||
const handlePlaySoundChange = useCallback(
|
||||
(playSound: boolean) => {
|
||||
void updateSettings({ notifications: { playSound } }).catch(() => {
|
||||
// useDesktopSettings owns the user-visible IPC error.
|
||||
});
|
||||
},
|
||||
[updateSettings],
|
||||
);
|
||||
|
||||
const handleSendTestNotification = useCallback(() => {
|
||||
void sendTestNotification();
|
||||
}, [sendTestNotification]);
|
||||
|
||||
const isPermissionBusy = isRefreshing || requestingPermission !== null;
|
||||
const isSendingTestNotification = testNotificationState.status === "sending";
|
||||
const refreshIcon = useMemo(() => <ThemedRotateCw />, []);
|
||||
const refreshButton = useMemo(
|
||||
() => (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
leftIcon={refreshIcon}
|
||||
onPress={handleRefreshPress}
|
||||
disabled={isPermissionBusy}
|
||||
accessibilityLabel={t("settings.notifications.refreshAccessibility")}
|
||||
>
|
||||
{isRefreshing ? t("settings.permissions.refreshing") : t("settings.permissions.refresh")}
|
||||
</Button>
|
||||
),
|
||||
[handleRefreshPress, isPermissionBusy, isRefreshing, refreshIcon, t],
|
||||
);
|
||||
const permissionLabels = useMemo(
|
||||
() => ({
|
||||
granted: t("settings.permissions.actions.granted"),
|
||||
request: t("settings.permissions.actions.request"),
|
||||
requesting: t("settings.permissions.actions.requesting"),
|
||||
}),
|
||||
[t],
|
||||
);
|
||||
|
||||
if (!isDesktopApp) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const notificationsGranted = snapshot?.notifications.state === "granted";
|
||||
|
||||
return (
|
||||
<SettingsSection title={t("settings.notifications.title")} trailing={refreshButton}>
|
||||
<View style={settingsStyles.card}>
|
||||
<DesktopPermissionRow
|
||||
title={t("settings.notifications.permission")}
|
||||
status={snapshot?.notifications ?? null}
|
||||
isRequesting={requestingPermission === "notifications"}
|
||||
onRequest={handleRequestNotifications}
|
||||
labels={permissionLabels}
|
||||
/>
|
||||
<View style={[settingsStyles.row, settingsStyles.rowBorder]}>
|
||||
<View style={settingsStyles.rowContent}>
|
||||
<Text style={settingsStyles.rowTitle}>{t("settings.notifications.playSound")}</Text>
|
||||
<Text style={settingsStyles.rowHint}>{t("settings.notifications.playSoundHint")}</Text>
|
||||
</View>
|
||||
<Switch
|
||||
value={settings.notifications.playSound}
|
||||
onValueChange={handlePlaySoundChange}
|
||||
disabled={isSaving}
|
||||
accessibilityLabel={t("settings.notifications.playSound")}
|
||||
testID="desktop-notifications-play-sound-switch"
|
||||
/>
|
||||
</View>
|
||||
<View style={[settingsStyles.row, settingsStyles.rowBorder]}>
|
||||
<View style={settingsStyles.rowContent}>
|
||||
<Text style={settingsStyles.rowTitle}>{t("settings.notifications.test")}</Text>
|
||||
<Text style={settingsStyles.rowHint}>
|
||||
{notificationsGranted
|
||||
? t("settings.notifications.testHint")
|
||||
: t("settings.notifications.permissionRequired")}
|
||||
</Text>
|
||||
</View>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onPress={handleSendTestNotification}
|
||||
disabled={!notificationsGranted || isPermissionBusy || isSendingTestNotification}
|
||||
>
|
||||
{isSendingTestNotification
|
||||
? t("settings.notifications.sending")
|
||||
: t("settings.notifications.send")}
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
{testNotificationState.status === "success" ? (
|
||||
<Alert
|
||||
variant="success"
|
||||
title={t("settings.notifications.sentTitle")}
|
||||
description={t("settings.notifications.sentDescription")}
|
||||
testID="desktop-notifications-test-success"
|
||||
/>
|
||||
) : null}
|
||||
{testNotificationState.status === "error" ? (
|
||||
<Alert
|
||||
variant="error"
|
||||
title={t("settings.notifications.sendFailedTitle")}
|
||||
description={testNotificationState.message}
|
||||
testID="desktop-notifications-test-error"
|
||||
/>
|
||||
) : null}
|
||||
</SettingsSection>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useMemo } from "react";
|
||||
import { View, Text } from "react-native";
|
||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||
import { StyleSheet, withUnistyles } from "react-native-unistyles";
|
||||
import { Check } from "lucide-react-native";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { settingsStyles } from "@/styles/settings";
|
||||
@@ -14,16 +14,16 @@ export interface DesktopPermissionRowProps {
|
||||
granted: string;
|
||||
request: string;
|
||||
requesting: string;
|
||||
busyExtraAction: (label: string) => string;
|
||||
};
|
||||
showBorder?: boolean;
|
||||
onRequest: () => void;
|
||||
extraActionLabel?: string;
|
||||
isExtraActionBusy?: boolean;
|
||||
isExtraActionDisabled?: boolean;
|
||||
onExtraAction?: () => void;
|
||||
}
|
||||
|
||||
const ThemedCheck = withUnistyles(Check, (theme) => ({
|
||||
size: theme.iconSize.sm,
|
||||
color: theme.colors.foregroundMuted,
|
||||
}));
|
||||
|
||||
export function DesktopPermissionRow({
|
||||
title,
|
||||
status,
|
||||
@@ -31,12 +31,7 @@ export function DesktopPermissionRow({
|
||||
labels,
|
||||
showBorder,
|
||||
onRequest,
|
||||
extraActionLabel,
|
||||
isExtraActionBusy = false,
|
||||
isExtraActionDisabled = false,
|
||||
onExtraAction,
|
||||
}: DesktopPermissionRowProps) {
|
||||
const { theme } = useUnistyles();
|
||||
const state = status?.state ?? "unknown";
|
||||
const isGranted = state === "granted";
|
||||
const shouldShowDetail =
|
||||
@@ -58,21 +53,9 @@ export function DesktopPermissionRow({
|
||||
</View>
|
||||
<View style={styles.permissionRowActions}>
|
||||
{isGranted ? (
|
||||
<View style={styles.permissionGrantedActions}>
|
||||
<View style={styles.permissionStatusPill}>
|
||||
<Check size={theme.iconSize.sm} color={theme.colors.foregroundMuted} />
|
||||
<Text style={styles.permissionStatusText}>{labels.granted}</Text>
|
||||
</View>
|
||||
{extraActionLabel && onExtraAction ? (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onPress={onExtraAction}
|
||||
disabled={isExtraActionDisabled || isExtraActionBusy}
|
||||
>
|
||||
{isExtraActionBusy ? labels.busyExtraAction(extraActionLabel) : extraActionLabel}
|
||||
</Button>
|
||||
) : null}
|
||||
<View style={styles.permissionStatusPill}>
|
||||
<ThemedCheck />
|
||||
<Text style={styles.permissionStatusText}>{labels.granted}</Text>
|
||||
</View>
|
||||
) : (
|
||||
<Button variant="outline" size="sm" onPress={onRequest} disabled={isRequesting}>
|
||||
@@ -92,11 +75,6 @@ const styles = StyleSheet.create((theme) => ({
|
||||
alignItems: "flex-end",
|
||||
gap: theme.spacing[1],
|
||||
},
|
||||
permissionGrantedActions: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: theme.spacing[2],
|
||||
},
|
||||
permissionStatusPill: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { View, Text } from "react-native";
|
||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||
import { View } from "react-native";
|
||||
import { withUnistyles } from "react-native-unistyles";
|
||||
import { RotateCw } from "lucide-react-native";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { DesktopPermissionRow } from "@/desktop/components/desktop-permission-row";
|
||||
@@ -9,49 +9,33 @@ import { useDesktopPermissions } from "@/desktop/permissions/use-desktop-permiss
|
||||
import { settingsStyles } from "@/styles/settings";
|
||||
import { SettingsSection } from "@/screens/settings/settings-section";
|
||||
|
||||
const ThemedRotateCw = withUnistyles(RotateCw, (theme) => ({
|
||||
size: theme.iconSize.md,
|
||||
color: theme.colors.foregroundMuted,
|
||||
}));
|
||||
|
||||
export function DesktopPermissionsSection() {
|
||||
const { t } = useTranslation();
|
||||
const { theme } = useUnistyles();
|
||||
const {
|
||||
isDesktopApp,
|
||||
snapshot,
|
||||
isRefreshing,
|
||||
requestingPermission,
|
||||
isSendingTestNotification,
|
||||
testNotificationError,
|
||||
refreshPermissions,
|
||||
requestPermission,
|
||||
sendTestNotification,
|
||||
} = useDesktopPermissions();
|
||||
|
||||
const errorTextStyle = useMemo(
|
||||
() => [styles.errorText, { color: theme.colors.destructive }],
|
||||
[theme.colors.destructive],
|
||||
);
|
||||
|
||||
const handleRefreshPress = useCallback(() => {
|
||||
void refreshPermissions();
|
||||
}, [refreshPermissions]);
|
||||
|
||||
const handleRequestNotifications = useCallback(() => {
|
||||
void requestPermission("notifications");
|
||||
}, [requestPermission]);
|
||||
|
||||
const handleRequestMicrophone = useCallback(() => {
|
||||
void requestPermission("microphone");
|
||||
}, [requestPermission]);
|
||||
|
||||
const handleSendTestNotification = useCallback(() => {
|
||||
void sendTestNotification();
|
||||
}, [sendTestNotification]);
|
||||
|
||||
const isBusy = isRefreshing || requestingPermission !== null;
|
||||
const notificationsGranted = snapshot?.notifications.state === "granted";
|
||||
|
||||
const refreshIcon = useMemo(
|
||||
() => <RotateCw size={theme.iconSize.md} color={theme.colors.foregroundMuted} />,
|
||||
[theme.iconSize.md, theme.colors.foregroundMuted],
|
||||
);
|
||||
const refreshIcon = useMemo(() => <ThemedRotateCw />, []);
|
||||
|
||||
const refreshButton = useMemo(
|
||||
() => (
|
||||
@@ -74,7 +58,6 @@ export function DesktopPermissionsSection() {
|
||||
granted: t("settings.permissions.actions.granted"),
|
||||
request: t("settings.permissions.actions.request"),
|
||||
requesting: t("settings.permissions.actions.requesting"),
|
||||
busyExtraAction: (label: string) => t("settings.permissions.actions.busySuffix", { label }),
|
||||
}),
|
||||
[t],
|
||||
);
|
||||
@@ -86,21 +69,8 @@ export function DesktopPermissionsSection() {
|
||||
return (
|
||||
<SettingsSection title={t("settings.permissions.title")} trailing={refreshButton}>
|
||||
<View style={settingsStyles.card}>
|
||||
<DesktopPermissionRow
|
||||
title={t("settings.permissions.notifications")}
|
||||
status={snapshot?.notifications ?? null}
|
||||
isRequesting={requestingPermission === "notifications"}
|
||||
onRequest={handleRequestNotifications}
|
||||
labels={permissionLabels}
|
||||
extraActionLabel={t("settings.permissions.test")}
|
||||
isExtraActionBusy={isSendingTestNotification}
|
||||
isExtraActionDisabled={!notificationsGranted || isBusy}
|
||||
onExtraAction={handleSendTestNotification}
|
||||
/>
|
||||
{testNotificationError ? <Text style={errorTextStyle}>{testNotificationError}</Text> : null}
|
||||
<DesktopPermissionRow
|
||||
title={t("settings.permissions.microphone")}
|
||||
showBorder
|
||||
status={snapshot?.microphone ?? null}
|
||||
isRequesting={requestingPermission === "microphone"}
|
||||
onRequest={handleRequestMicrophone}
|
||||
@@ -110,11 +80,3 @@ export function DesktopPermissionsSection() {
|
||||
</SettingsSection>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create((theme) => ({
|
||||
errorText: {
|
||||
fontSize: theme.fontSize.xs,
|
||||
paddingHorizontal: theme.spacing[4],
|
||||
paddingBottom: theme.spacing[2],
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -14,13 +14,18 @@ export interface UseDesktopPermissionsReturn {
|
||||
snapshot: DesktopPermissionSnapshot | null;
|
||||
isRefreshing: boolean;
|
||||
requestingPermission: DesktopPermissionKind | null;
|
||||
isSendingTestNotification: boolean;
|
||||
testNotificationError: string | null;
|
||||
testNotificationState: TestNotificationState;
|
||||
refreshPermissions: () => Promise<void>;
|
||||
requestPermission: (kind: DesktopPermissionKind) => Promise<void>;
|
||||
sendTestNotification: () => Promise<void>;
|
||||
}
|
||||
|
||||
export type TestNotificationState =
|
||||
| { status: "idle" }
|
||||
| { status: "sending" }
|
||||
| { status: "success" }
|
||||
| { status: "error"; message: string };
|
||||
|
||||
export function useDesktopPermissions(): UseDesktopPermissionsReturn {
|
||||
const { t } = useTranslation();
|
||||
const isDesktopApp = shouldShowDesktopPermissionSection();
|
||||
@@ -30,7 +35,9 @@ export function useDesktopPermissions(): UseDesktopPermissionsReturn {
|
||||
const [requestingPermission, setRequestingPermission] = useState<DesktopPermissionKind | null>(
|
||||
null,
|
||||
);
|
||||
const [isSendingTestNotification, setIsSendingTestNotification] = useState(false);
|
||||
const [testNotificationState, setTestNotificationState] = useState<TestNotificationState>({
|
||||
status: "idle",
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
@@ -111,28 +118,34 @@ export function useDesktopPermissions(): UseDesktopPermissionsReturn {
|
||||
[isDesktopApp, refreshPermissions, t],
|
||||
);
|
||||
|
||||
const [testNotificationError, setTestNotificationError] = useState<string | null>(null);
|
||||
|
||||
const sendTestNotification = useCallback(async () => {
|
||||
if (!isDesktopApp) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSendingTestNotification(true);
|
||||
setTestNotificationError(null);
|
||||
setTestNotificationState({ status: "sending" });
|
||||
try {
|
||||
const sent = await sendOsNotification({
|
||||
title: t("desktop.permissions.testNotification.title"),
|
||||
body: t("desktop.permissions.testNotification.body"),
|
||||
});
|
||||
if (!sent) {
|
||||
setTestNotificationError(t("desktop.permissions.testNotification.notDelivered"));
|
||||
if (!isMountedRef.current) {
|
||||
return;
|
||||
}
|
||||
setTestNotificationState(
|
||||
sent
|
||||
? { status: "success" }
|
||||
: {
|
||||
status: "error",
|
||||
message: t("desktop.permissions.testNotification.notDelivered"),
|
||||
},
|
||||
);
|
||||
} catch {
|
||||
setTestNotificationError(t("desktop.permissions.testNotification.failed"));
|
||||
} finally {
|
||||
if (isMountedRef.current) {
|
||||
setIsSendingTestNotification(false);
|
||||
setTestNotificationState({
|
||||
status: "error",
|
||||
message: t("desktop.permissions.testNotification.failed"),
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [isDesktopApp, t]);
|
||||
@@ -150,8 +163,7 @@ export function useDesktopPermissions(): UseDesktopPermissionsReturn {
|
||||
snapshot,
|
||||
isRefreshing,
|
||||
requestingPermission,
|
||||
isSendingTestNotification,
|
||||
testNotificationError,
|
||||
testNotificationState,
|
||||
refreshPermissions,
|
||||
requestPermission,
|
||||
sendTestNotification,
|
||||
|
||||
@@ -13,6 +13,9 @@ const DESKTOP_SETTINGS_QUERY_KEY = ["desktop-settings"] as const;
|
||||
|
||||
export interface DesktopSettings {
|
||||
releaseChannel: ReleaseChannel;
|
||||
notifications: {
|
||||
playSound: boolean;
|
||||
};
|
||||
daemon: {
|
||||
manageBuiltInDaemon: boolean;
|
||||
keepRunningAfterQuit: boolean;
|
||||
@@ -21,11 +24,15 @@ export interface DesktopSettings {
|
||||
|
||||
export interface DesktopSettingsPatch {
|
||||
releaseChannel?: ReleaseChannel;
|
||||
notifications?: Partial<DesktopSettings["notifications"]>;
|
||||
daemon?: Partial<DesktopSettings["daemon"]>;
|
||||
}
|
||||
|
||||
export const DEFAULT_DESKTOP_SETTINGS: DesktopSettings = {
|
||||
releaseChannel: "stable",
|
||||
notifications: {
|
||||
playSound: true,
|
||||
},
|
||||
daemon: {
|
||||
manageBuiltInDaemon: true,
|
||||
keepRunningAfterQuit: false,
|
||||
@@ -144,10 +151,17 @@ export async function migrateLegacyDesktopSettings(input: {
|
||||
|
||||
function parseDesktopSettings(raw: unknown): DesktopSettings {
|
||||
const record = isRecord(raw) ? raw : {};
|
||||
const notifications = isRecord(record.notifications) ? record.notifications : {};
|
||||
const daemon = isRecord(record.daemon) ? record.daemon : {};
|
||||
|
||||
return {
|
||||
releaseChannel: record.releaseChannel === "beta" ? "beta" : "stable",
|
||||
notifications: {
|
||||
playSound:
|
||||
typeof notifications.playSound === "boolean"
|
||||
? notifications.playSound
|
||||
: DEFAULT_DESKTOP_SETTINGS.notifications.playSound,
|
||||
},
|
||||
daemon: {
|
||||
manageBuiltInDaemon:
|
||||
typeof daemon.manageBuiltInDaemon === "boolean"
|
||||
@@ -167,6 +181,10 @@ function mergeDesktopSettings(
|
||||
): DesktopSettings {
|
||||
return {
|
||||
releaseChannel: updates.releaseChannel ?? current.releaseChannel,
|
||||
notifications: {
|
||||
...current.notifications,
|
||||
...updates.notifications,
|
||||
},
|
||||
daemon: {
|
||||
...current.daemon,
|
||||
...updates.daemon,
|
||||
@@ -177,6 +195,7 @@ function mergeDesktopSettings(
|
||||
function normalizePatch(updates: DesktopSettingsPatch): Record<string, unknown> {
|
||||
return {
|
||||
...(updates.releaseChannel ? { releaseChannel: updates.releaseChannel } : {}),
|
||||
...(updates.notifications ? { notifications: updates.notifications } : {}),
|
||||
...(updates.daemon ? { daemon: updates.daemon } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -29,6 +29,9 @@ export interface FakeDesktopBridge extends DesktopSettingsBridge {
|
||||
|
||||
const DEFAULT_DESKTOP: DesktopSettings = {
|
||||
releaseChannel: "stable",
|
||||
notifications: {
|
||||
playSound: true,
|
||||
},
|
||||
daemon: {
|
||||
manageBuiltInDaemon: true,
|
||||
keepRunningAfterQuit: false,
|
||||
|
||||
@@ -255,6 +255,7 @@ describe("loadSettingsFromStorage", () => {
|
||||
isElectron: true,
|
||||
settings: {
|
||||
releaseChannel: "beta",
|
||||
notifications: { playSound: true },
|
||||
daemon: { manageBuiltInDaemon: false, keepRunningAfterQuit: true },
|
||||
},
|
||||
});
|
||||
|
||||
@@ -246,7 +246,9 @@ describe("translation resources", () => {
|
||||
expect(en.settings.shortcuts.actions.rebind).toBe("Rebind");
|
||||
expect(en.settings.integrations.commandLine.title).toBe("Command line");
|
||||
expect(en.settings.integrations.skills.updateAvailable).toBe("Update available");
|
||||
expect(en.settings.permissions.notifications).toBe("Notifications");
|
||||
expect(en.settings.notifications.playSound).toBe("Play sound");
|
||||
expect(en.settings.notifications.permission).toBe("Notification permission");
|
||||
expect(en.settings.notifications.sentTitle).toBe("Test notification sent");
|
||||
expect(en.settings.permissions.actions.request).toBe("Request");
|
||||
});
|
||||
|
||||
|
||||
@@ -1703,6 +1703,7 @@ export const ar: TranslationResources = {
|
||||
editor: "المحرر",
|
||||
shortcuts: "الاختصارات",
|
||||
integrations: "التكامل",
|
||||
notifications: "الإشعارات",
|
||||
permissions: "الأذونات",
|
||||
diagnostics: "التشخيص",
|
||||
about: "عن",
|
||||
@@ -1712,6 +1713,21 @@ export const ar: TranslationResources = {
|
||||
vimKeybindings: "اختصارات Vim",
|
||||
vimHint: "تنطبق على ملفات المصدر في الويب وسطح المكتب.",
|
||||
},
|
||||
notifications: {
|
||||
title: "الإشعارات",
|
||||
permission: "إذن الإشعارات",
|
||||
refreshAccessibility: "تحديث إذن الإشعارات",
|
||||
playSound: "تشغيل صوت",
|
||||
playSoundHint: "تشغيل صوت عند وصول إشعار سطح مكتب",
|
||||
test: "اختبار الإشعار",
|
||||
testHint: "إرسال إشعار باستخدام هذه الإعدادات",
|
||||
permissionRequired: "اسمح بالوصول إلى الإشعارات قبل الاختبار",
|
||||
send: "إرسال",
|
||||
sending: "جارٍ الإرسال...",
|
||||
sentTitle: "تم إرسال إشعار الاختبار",
|
||||
sentDescription: "سلّم Paseo الإشعار إلى نظام التشغيل.",
|
||||
sendFailedTitle: "تعذر إرسال إشعار الاختبار",
|
||||
},
|
||||
hostSections: {
|
||||
projects: "المشاريع",
|
||||
connections: "اتصالات",
|
||||
@@ -2006,17 +2022,14 @@ export const ar: TranslationResources = {
|
||||
},
|
||||
permissions: {
|
||||
title: "الأذونات",
|
||||
notifications: "إشعارات",
|
||||
microphone: "ميكروفون",
|
||||
refresh: "ينعش",
|
||||
refreshing: "منعش...",
|
||||
refreshAccessibility: "تحديث أذونات سطح المكتب",
|
||||
test: "امتحان",
|
||||
actions: {
|
||||
granted: "ممنوح",
|
||||
request: "طلب",
|
||||
requesting: "جارٍ الطلب...",
|
||||
busySuffix: "{{label}}...",
|
||||
},
|
||||
},
|
||||
host: {
|
||||
|
||||
@@ -1714,6 +1714,7 @@ export const en = {
|
||||
editor: "Editor",
|
||||
shortcuts: "Shortcuts",
|
||||
integrations: "Integrations",
|
||||
notifications: "Notifications",
|
||||
permissions: "Permissions",
|
||||
diagnostics: "Diagnostics",
|
||||
about: "About",
|
||||
@@ -1723,6 +1724,21 @@ export const en = {
|
||||
vimKeybindings: "Vim keybindings",
|
||||
vimHint: "Applies to source files on web and desktop.",
|
||||
},
|
||||
notifications: {
|
||||
title: "Notifications",
|
||||
permission: "Notification permission",
|
||||
refreshAccessibility: "Refresh notification permission",
|
||||
playSound: "Play sound",
|
||||
playSoundHint: "Play a sound when a desktop notification arrives",
|
||||
test: "Test notification",
|
||||
testHint: "Send a notification using these settings",
|
||||
permissionRequired: "Allow notification access before testing",
|
||||
send: "Send",
|
||||
sending: "Sending...",
|
||||
sentTitle: "Test notification sent",
|
||||
sentDescription: "Paseo handed the notification to the operating system.",
|
||||
sendFailedTitle: "Unable to send test notification",
|
||||
},
|
||||
hostSections: {
|
||||
projects: "Projects",
|
||||
connections: "Connections",
|
||||
@@ -2017,17 +2033,14 @@ export const en = {
|
||||
},
|
||||
permissions: {
|
||||
title: "Permissions",
|
||||
notifications: "Notifications",
|
||||
microphone: "Microphone",
|
||||
refresh: "Refresh",
|
||||
refreshing: "Refreshing...",
|
||||
refreshAccessibility: "Refresh desktop permissions",
|
||||
test: "Test",
|
||||
actions: {
|
||||
granted: "Granted",
|
||||
request: "Request",
|
||||
requesting: "Requesting...",
|
||||
busySuffix: "{{label}}...",
|
||||
},
|
||||
},
|
||||
host: {
|
||||
|
||||
@@ -1746,6 +1746,7 @@ export const es: TranslationResources = {
|
||||
editor: "Editor",
|
||||
shortcuts: "Atajos",
|
||||
integrations: "Integraciones",
|
||||
notifications: "Notificaciones",
|
||||
permissions: "Permisos",
|
||||
diagnostics: "Diagnóstico",
|
||||
about: "Acerca de",
|
||||
@@ -1755,6 +1756,21 @@ export const es: TranslationResources = {
|
||||
vimKeybindings: "Atajos de Vim",
|
||||
vimHint: "Se aplica a archivos fuente en web y escritorio.",
|
||||
},
|
||||
notifications: {
|
||||
title: "Notificaciones",
|
||||
permission: "Permiso de notificaciones",
|
||||
refreshAccessibility: "Actualizar permiso de notificaciones",
|
||||
playSound: "Reproducir sonido",
|
||||
playSoundHint: "Reproduce un sonido cuando llega una notificación de escritorio",
|
||||
test: "Probar notificación",
|
||||
testHint: "Envía una notificación con esta configuración",
|
||||
permissionRequired: "Permite el acceso a las notificaciones antes de probar",
|
||||
send: "Enviar",
|
||||
sending: "Enviando...",
|
||||
sentTitle: "Notificación de prueba enviada",
|
||||
sentDescription: "Paseo entregó la notificación al sistema operativo.",
|
||||
sendFailedTitle: "No se pudo enviar la notificación de prueba",
|
||||
},
|
||||
hostSections: {
|
||||
projects: "Proyectos",
|
||||
connections: "Conexiones",
|
||||
@@ -2056,17 +2072,14 @@ export const es: TranslationResources = {
|
||||
},
|
||||
permissions: {
|
||||
title: "Permisos",
|
||||
notifications: "Notificaciones",
|
||||
microphone: "Micrófono",
|
||||
refresh: "Refrescar",
|
||||
refreshing: "Refrescante...",
|
||||
refreshAccessibility: "Actualizar permisos de escritorio",
|
||||
test: "Prueba",
|
||||
actions: {
|
||||
granted: "Otorgada",
|
||||
request: "Pedido",
|
||||
requesting: "Solicitando...",
|
||||
busySuffix: "{{label}}...",
|
||||
},
|
||||
},
|
||||
host: {
|
||||
|
||||
@@ -1750,6 +1750,7 @@ export const fr: TranslationResources = {
|
||||
editor: "Éditeur",
|
||||
shortcuts: "Raccourcis",
|
||||
integrations: "Intégrations",
|
||||
notifications: "Notifications",
|
||||
permissions: "Autorisations",
|
||||
diagnostics: "Diagnostic",
|
||||
about: "À propos",
|
||||
@@ -1759,6 +1760,21 @@ export const fr: TranslationResources = {
|
||||
vimKeybindings: "Raccourcis Vim",
|
||||
vimHint: "S’applique aux fichiers source sur le web et le bureau.",
|
||||
},
|
||||
notifications: {
|
||||
title: "Notifications",
|
||||
permission: "Autorisation des notifications",
|
||||
refreshAccessibility: "Actualiser l’autorisation des notifications",
|
||||
playSound: "Émettre un son",
|
||||
playSoundHint: "Émet un son lorsqu’une notification de bureau arrive",
|
||||
test: "Tester la notification",
|
||||
testHint: "Envoie une notification avec ces réglages",
|
||||
permissionRequired: "Autorisez l’accès aux notifications avant le test",
|
||||
send: "Envoyer",
|
||||
sending: "Envoi...",
|
||||
sentTitle: "Notification de test envoyée",
|
||||
sentDescription: "Paseo a transmis la notification au système d’exploitation.",
|
||||
sendFailedTitle: "Impossible d’envoyer la notification de test",
|
||||
},
|
||||
hostSections: {
|
||||
projects: "Projets",
|
||||
connections: "Relations",
|
||||
@@ -2060,17 +2076,14 @@ export const fr: TranslationResources = {
|
||||
},
|
||||
permissions: {
|
||||
title: "Autorisations",
|
||||
notifications: "Notifications",
|
||||
microphone: "Microphone",
|
||||
refresh: "Rafraîchir",
|
||||
refreshing: "Rafraîchissant...",
|
||||
refreshAccessibility: "Actualiser les autorisations du bureau",
|
||||
test: "Test",
|
||||
actions: {
|
||||
granted: "Accordé",
|
||||
request: "Demande",
|
||||
requesting: "Demander...",
|
||||
busySuffix: "{{label}}...",
|
||||
},
|
||||
},
|
||||
host: {
|
||||
|
||||
@@ -1719,6 +1719,7 @@ export const ja: TranslationResources = {
|
||||
editor: "エディター",
|
||||
shortcuts: "ショートカット",
|
||||
integrations: "連携",
|
||||
notifications: "通知",
|
||||
permissions: "権限",
|
||||
diagnostics: "診断",
|
||||
about: "アプリ情報",
|
||||
@@ -1728,6 +1729,21 @@ export const ja: TranslationResources = {
|
||||
vimKeybindings: "Vim キーバインド",
|
||||
vimHint: "Web とデスクトップのソースファイルに適用されます。",
|
||||
},
|
||||
notifications: {
|
||||
title: "通知",
|
||||
permission: "通知の権限",
|
||||
refreshAccessibility: "通知の権限を更新",
|
||||
playSound: "サウンドを再生",
|
||||
playSoundHint: "デスクトップ通知が届いたときにサウンドを再生します",
|
||||
test: "通知をテスト",
|
||||
testHint: "現在の設定でテスト通知を送信します",
|
||||
permissionRequired: "テストする前に通知へのアクセスを許可してください",
|
||||
send: "送信",
|
||||
sending: "送信中...",
|
||||
sentTitle: "テスト通知を送信しました",
|
||||
sentDescription: "Paseo が通知をオペレーティングシステムに渡しました。",
|
||||
sendFailedTitle: "テスト通知を送信できません",
|
||||
},
|
||||
hostSections: {
|
||||
projects: "プロジェクト",
|
||||
connections: "接続",
|
||||
@@ -2025,17 +2041,14 @@ export const ja: TranslationResources = {
|
||||
},
|
||||
permissions: {
|
||||
title: "権限",
|
||||
notifications: "通知",
|
||||
microphone: "マイク",
|
||||
refresh: "更新",
|
||||
refreshing: "更新中...",
|
||||
refreshAccessibility: "デスクトップの権限を更新",
|
||||
test: "テスト",
|
||||
actions: {
|
||||
granted: "許可済み",
|
||||
request: "許可を求める",
|
||||
requesting: "許可を求めています...",
|
||||
busySuffix: "{{label}}...",
|
||||
},
|
||||
},
|
||||
host: {
|
||||
|
||||
@@ -1713,6 +1713,7 @@ export const ko: TranslationResources = {
|
||||
editor: "편집기",
|
||||
shortcuts: "단축키",
|
||||
integrations: "통합",
|
||||
notifications: "알림",
|
||||
permissions: "권한",
|
||||
diagnostics: "진단",
|
||||
about: "정보",
|
||||
@@ -1722,6 +1723,21 @@ export const ko: TranslationResources = {
|
||||
vimKeybindings: "Vim 키 바인딩",
|
||||
vimHint: "웹 및 데스크톱의 소스 파일에 적용됩니다.",
|
||||
},
|
||||
notifications: {
|
||||
title: "알림",
|
||||
permission: "알림 권한",
|
||||
refreshAccessibility: "알림 권한 새로 고침",
|
||||
playSound: "소리 재생",
|
||||
playSoundHint: "데스크톱 알림이 도착하면 소리를 재생합니다",
|
||||
test: "알림 테스트",
|
||||
testHint: "현재 설정으로 테스트 알림을 보냅니다",
|
||||
permissionRequired: "테스트하기 전에 알림 접근을 허용하세요",
|
||||
send: "보내기",
|
||||
sending: "보내는 중...",
|
||||
sentTitle: "테스트 알림을 보냈습니다",
|
||||
sentDescription: "Paseo가 알림을 운영 체제에 전달했습니다.",
|
||||
sendFailedTitle: "테스트 알림을 보낼 수 없음",
|
||||
},
|
||||
hostSections: {
|
||||
projects: "프로젝트",
|
||||
connections: "연결",
|
||||
@@ -2018,17 +2034,14 @@ export const ko: TranslationResources = {
|
||||
},
|
||||
permissions: {
|
||||
title: "권한",
|
||||
notifications: "알림",
|
||||
microphone: "마이크",
|
||||
refresh: "새로고침",
|
||||
refreshing: "새로고침 중...",
|
||||
refreshAccessibility: "데스크톱 권한 새로고침",
|
||||
test: "테스트",
|
||||
actions: {
|
||||
granted: "허용됨",
|
||||
request: "요청",
|
||||
requesting: "요청 중...",
|
||||
busySuffix: "{{label}}...",
|
||||
},
|
||||
},
|
||||
host: {
|
||||
|
||||
@@ -1732,6 +1732,7 @@ export const ptBR: TranslationResources = {
|
||||
editor: "Editor",
|
||||
shortcuts: "Atalhos",
|
||||
integrations: "Integrações",
|
||||
notifications: "Notificações",
|
||||
permissions: "Permissões",
|
||||
diagnostics: "Diagnósticos",
|
||||
about: "Sobre",
|
||||
@@ -1741,6 +1742,21 @@ export const ptBR: TranslationResources = {
|
||||
vimKeybindings: "Atalhos do Vim",
|
||||
vimHint: "Aplica-se a arquivos-fonte na web e no desktop.",
|
||||
},
|
||||
notifications: {
|
||||
title: "Notificações",
|
||||
permission: "Permissão de notificações",
|
||||
refreshAccessibility: "Atualizar permissão de notificações",
|
||||
playSound: "Reproduzir som",
|
||||
playSoundHint: "Reproduz um som quando chega uma notificação da área de trabalho",
|
||||
test: "Testar notificação",
|
||||
testHint: "Envia uma notificação usando estas configurações",
|
||||
permissionRequired: "Permita o acesso às notificações antes de testar",
|
||||
send: "Enviar",
|
||||
sending: "Enviando...",
|
||||
sentTitle: "Notificação de teste enviada",
|
||||
sentDescription: "O Paseo entregou a notificação ao sistema operacional.",
|
||||
sendFailedTitle: "Não foi possível enviar a notificação de teste",
|
||||
},
|
||||
hostSections: {
|
||||
projects: "Projetos",
|
||||
connections: "Conexões",
|
||||
@@ -2040,17 +2056,14 @@ export const ptBR: TranslationResources = {
|
||||
},
|
||||
permissions: {
|
||||
title: "Permissões",
|
||||
notifications: "Notificações",
|
||||
microphone: "Microfone",
|
||||
refresh: "Atualizar",
|
||||
refreshing: "Atualizando...",
|
||||
refreshAccessibility: "Atualizar permissões do desktop",
|
||||
test: "Testar",
|
||||
actions: {
|
||||
granted: "Concedida",
|
||||
request: "Solicitar",
|
||||
requesting: "Solicitando...",
|
||||
busySuffix: "{{label}}...",
|
||||
},
|
||||
},
|
||||
host: {
|
||||
|
||||
@@ -1737,6 +1737,7 @@ export const ru: TranslationResources = {
|
||||
editor: "Редактор",
|
||||
shortcuts: "Ярлыки",
|
||||
integrations: "Интеграции",
|
||||
notifications: "Уведомления",
|
||||
permissions: "Разрешения",
|
||||
diagnostics: "Диагностика",
|
||||
about: "О",
|
||||
@@ -1746,6 +1747,21 @@ export const ru: TranslationResources = {
|
||||
vimKeybindings: "Клавиши Vim",
|
||||
vimHint: "Применяется к исходным файлам в веб- и настольной версии.",
|
||||
},
|
||||
notifications: {
|
||||
title: "Уведомления",
|
||||
permission: "Разрешение на уведомления",
|
||||
refreshAccessibility: "Обновить разрешение на уведомления",
|
||||
playSound: "Воспроизводить звук",
|
||||
playSoundHint: "Воспроизводит звук при получении уведомления на компьютере",
|
||||
test: "Проверить уведомление",
|
||||
testHint: "Отправляет уведомление с текущими настройками",
|
||||
permissionRequired: "Разрешите доступ к уведомлениям перед проверкой",
|
||||
send: "Отправить",
|
||||
sending: "Отправка...",
|
||||
sentTitle: "Тестовое уведомление отправлено",
|
||||
sentDescription: "Paseo передал уведомление операционной системе.",
|
||||
sendFailedTitle: "Не удалось отправить тестовое уведомление",
|
||||
},
|
||||
hostSections: {
|
||||
projects: "Проекты",
|
||||
connections: "Соединения",
|
||||
@@ -2044,17 +2060,14 @@ export const ru: TranslationResources = {
|
||||
},
|
||||
permissions: {
|
||||
title: "Разрешения",
|
||||
notifications: "Уведомления",
|
||||
microphone: "Микрофон",
|
||||
refresh: "Обновить",
|
||||
refreshing: "Освежающий...",
|
||||
refreshAccessibility: "Обновить разрешения рабочего стола",
|
||||
test: "Тест",
|
||||
actions: {
|
||||
granted: "Предоставленный",
|
||||
request: "Запрос",
|
||||
requesting: "Запрос...",
|
||||
busySuffix: "{{label}}...",
|
||||
},
|
||||
},
|
||||
host: {
|
||||
|
||||
@@ -1683,6 +1683,7 @@ export const zhCN: TranslationResources = {
|
||||
editor: "编辑器",
|
||||
shortcuts: "快捷键",
|
||||
integrations: "集成",
|
||||
notifications: "通知",
|
||||
permissions: "权限",
|
||||
diagnostics: "诊断",
|
||||
about: "关于",
|
||||
@@ -1692,6 +1693,21 @@ export const zhCN: TranslationResources = {
|
||||
vimKeybindings: "Vim 键位",
|
||||
vimHint: "适用于网页和桌面端的源文件。",
|
||||
},
|
||||
notifications: {
|
||||
title: "通知",
|
||||
permission: "通知权限",
|
||||
refreshAccessibility: "刷新通知权限",
|
||||
playSound: "播放声音",
|
||||
playSoundHint: "收到桌面通知时播放声音",
|
||||
test: "测试通知",
|
||||
testHint: "使用当前设置发送测试通知",
|
||||
permissionRequired: "测试前请允许访问通知",
|
||||
send: "发送",
|
||||
sending: "正在发送...",
|
||||
sentTitle: "测试通知已发送",
|
||||
sentDescription: "Paseo 已将通知交给操作系统。",
|
||||
sendFailedTitle: "无法发送测试通知",
|
||||
},
|
||||
hostSections: {
|
||||
projects: "项目",
|
||||
connections: "连接",
|
||||
@@ -1984,17 +2000,14 @@ export const zhCN: TranslationResources = {
|
||||
},
|
||||
permissions: {
|
||||
title: "权限",
|
||||
notifications: "通知",
|
||||
microphone: "麦克风",
|
||||
refresh: "刷新",
|
||||
refreshing: "正在刷新...",
|
||||
refreshAccessibility: "刷新桌面端权限",
|
||||
test: "测试",
|
||||
actions: {
|
||||
granted: "已授权",
|
||||
request: "请求",
|
||||
requesting: "正在请求...",
|
||||
busySuffix: "{{label}}...",
|
||||
},
|
||||
},
|
||||
host: {
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
Keyboard,
|
||||
Stethoscope,
|
||||
Info,
|
||||
Bell,
|
||||
Shield,
|
||||
Puzzle,
|
||||
Plus,
|
||||
@@ -78,6 +79,7 @@ import { CommunityLinks } from "@/components/community-links";
|
||||
import { SegmentedControl } from "@/components/ui/segmented-control";
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem } from "@/components/ui/dropdown-menu";
|
||||
import { DesktopPermissionsSection } from "@/desktop/components/desktop-permissions-section";
|
||||
import { DesktopNotificationsSection } from "@/desktop/components/desktop-notifications-section";
|
||||
import { BrowserDataSection } from "@/desktop/browser/settings/browser-data-section";
|
||||
import { IntegrationsSection } from "@/desktop/components/integrations-section";
|
||||
import { isElectronRuntime } from "@/desktop/host";
|
||||
@@ -154,6 +156,12 @@ const SIDEBAR_SECTION_ITEMS: SidebarSectionItem[] = [
|
||||
icon: Puzzle,
|
||||
desktopOnly: true,
|
||||
},
|
||||
{
|
||||
id: "notifications",
|
||||
labelKey: "settings.sections.notifications",
|
||||
icon: Bell,
|
||||
desktopOnly: true,
|
||||
},
|
||||
{
|
||||
id: "permissions",
|
||||
labelKey: "settings.sections.permissions",
|
||||
@@ -1427,6 +1435,8 @@ export default function SettingsScreen({ view, openAddHostIntent = null }: Setti
|
||||
return isDesktopApp ? <KeyboardShortcutsSection /> : null;
|
||||
case "integrations":
|
||||
return isDesktopApp ? <IntegrationsSection /> : null;
|
||||
case "notifications":
|
||||
return isDesktopApp ? <DesktopNotificationsSection /> : null;
|
||||
case "permissions":
|
||||
return isDesktopApp ? <DesktopPermissionsSection /> : null;
|
||||
case "diagnostics":
|
||||
|
||||
@@ -262,6 +262,10 @@ describe("host settings section slugs", () => {
|
||||
});
|
||||
|
||||
describe("settings section slugs", () => {
|
||||
it("includes desktop notification settings", () => {
|
||||
expect(isSettingsSectionSlug("notifications")).toBe(true);
|
||||
});
|
||||
|
||||
it("no longer treats daemon as a valid app-level settings section", () => {
|
||||
expect(isSettingsSectionSlug("daemon")).toBe(false);
|
||||
});
|
||||
|
||||
@@ -495,6 +495,7 @@ export const SETTINGS_SECTION_SLUGS = [
|
||||
"editor",
|
||||
"shortcuts",
|
||||
"integrations",
|
||||
"notifications",
|
||||
"permissions",
|
||||
"diagnostics",
|
||||
"about",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import path from "node:path";
|
||||
import { existsSync } from "node:fs";
|
||||
import { app, BrowserWindow, Notification, ipcMain, nativeImage } from "electron";
|
||||
import { getDesktopSettingsStore } from "../settings/desktop-settings-electron.js";
|
||||
|
||||
interface NotificationInput {
|
||||
title?: unknown;
|
||||
@@ -95,11 +96,12 @@ export function registerNotificationHandlers(): void {
|
||||
const body = toTrimmedString(rawInput?.body) ?? undefined;
|
||||
const data = toRecord(rawInput?.data);
|
||||
const icon = getNotificationIcon();
|
||||
const settings = await getDesktopSettingsStore().get();
|
||||
const notification = new Notification({
|
||||
title,
|
||||
...(body ? { body } : {}),
|
||||
...(icon ? { icon } : {}),
|
||||
silent: true,
|
||||
silent: !settings.notifications.playSound,
|
||||
});
|
||||
|
||||
activeNotifications.add(notification);
|
||||
|
||||
@@ -81,6 +81,7 @@ describe("desktop-settings", () => {
|
||||
|
||||
expect(settings).toEqual({
|
||||
releaseChannel: "stable",
|
||||
notifications: { playSound: true },
|
||||
daemon: {
|
||||
manageBuiltInDaemon: true,
|
||||
keepRunningAfterQuit: false,
|
||||
@@ -102,6 +103,7 @@ describe("desktop-settings", () => {
|
||||
|
||||
expect(next).toEqual({
|
||||
releaseChannel: "beta",
|
||||
notifications: { playSound: true },
|
||||
daemon: {
|
||||
manageBuiltInDaemon: true,
|
||||
keepRunningAfterQuit: false,
|
||||
@@ -110,6 +112,45 @@ describe("desktop-settings", () => {
|
||||
expect(files).toEqual(["desktop-settings.json"]);
|
||||
});
|
||||
|
||||
it("defaults notification sounds on for existing settings documents", async () => {
|
||||
const userDataPath = await createTempUserDataDir();
|
||||
directories.add(userDataPath);
|
||||
await writeFile(
|
||||
settingsFilePath(userDataPath),
|
||||
JSON.stringify({
|
||||
version: 1,
|
||||
settings: {
|
||||
releaseChannel: "stable",
|
||||
daemon: {
|
||||
manageBuiltInDaemon: true,
|
||||
keepRunningAfterQuit: false,
|
||||
},
|
||||
},
|
||||
migrations: {
|
||||
legacyRendererSettingsImported: true,
|
||||
daemonStopOnQuitDefaultApplied: true,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const settings = await createDesktopSettingsStore({ userDataPath }).get();
|
||||
|
||||
expect(settings.notifications.playSound).toBe(true);
|
||||
});
|
||||
|
||||
it("keeps an explicit notification sound choice across restarts", async () => {
|
||||
const userDataPath = await createTempUserDataDir();
|
||||
directories.add(userDataPath);
|
||||
|
||||
await createDesktopSettingsStore({ userDataPath }).patch({
|
||||
notifications: { playSound: false },
|
||||
});
|
||||
|
||||
const settings = await createDesktopSettingsStore({ userDataPath }).get();
|
||||
|
||||
expect(settings.notifications.playSound).toBe(false);
|
||||
});
|
||||
|
||||
it("does not let stale legacy renderer settings override an explicit desktop patch", async () => {
|
||||
const userDataPath = await createTempUserDataDir();
|
||||
directories.add(userDataPath);
|
||||
@@ -229,6 +270,7 @@ describe("desktop-settings", () => {
|
||||
|
||||
expect(migrated).toEqual({
|
||||
releaseChannel: "beta",
|
||||
notifications: { playSound: true },
|
||||
daemon: {
|
||||
manageBuiltInDaemon: false,
|
||||
keepRunningAfterQuit: false,
|
||||
|
||||
@@ -6,6 +6,9 @@ import type { AppReleaseChannel } from "../features/auto-updater.js";
|
||||
|
||||
export interface DesktopSettings {
|
||||
releaseChannel: AppReleaseChannel;
|
||||
notifications: {
|
||||
playSound: boolean;
|
||||
};
|
||||
daemon: {
|
||||
manageBuiltInDaemon: boolean;
|
||||
keepRunningAfterQuit: boolean;
|
||||
@@ -14,6 +17,7 @@ export interface DesktopSettings {
|
||||
|
||||
interface DesktopSettingsPatch {
|
||||
releaseChannel?: AppReleaseChannel;
|
||||
notifications?: Partial<DesktopSettings["notifications"]>;
|
||||
daemon?: Partial<DesktopSettings["daemon"]>;
|
||||
}
|
||||
|
||||
@@ -38,6 +42,9 @@ export interface DesktopSettingsStore {
|
||||
|
||||
export const DEFAULT_DESKTOP_SETTINGS: DesktopSettings = {
|
||||
releaseChannel: "stable",
|
||||
notifications: {
|
||||
playSound: true,
|
||||
},
|
||||
daemon: {
|
||||
manageBuiltInDaemon: true,
|
||||
keepRunningAfterQuit: false,
|
||||
@@ -73,6 +80,7 @@ function buildDefaultDocument(): PersistedDesktopSettingsDocument {
|
||||
version: 1,
|
||||
settings: {
|
||||
releaseChannel: DEFAULT_DESKTOP_SETTINGS.releaseChannel,
|
||||
notifications: { ...DEFAULT_DESKTOP_SETTINGS.notifications },
|
||||
daemon: { ...DEFAULT_DESKTOP_SETTINGS.daemon },
|
||||
},
|
||||
migrations: {
|
||||
@@ -85,6 +93,7 @@ function buildDefaultDocument(): PersistedDesktopSettingsDocument {
|
||||
function coerceDesktopSettings(input: unknown): DesktopSettings {
|
||||
const result: DesktopSettings = {
|
||||
releaseChannel: DEFAULT_DESKTOP_SETTINGS.releaseChannel,
|
||||
notifications: { ...DEFAULT_DESKTOP_SETTINGS.notifications },
|
||||
daemon: { ...DEFAULT_DESKTOP_SETTINGS.daemon },
|
||||
};
|
||||
|
||||
@@ -97,6 +106,13 @@ function coerceDesktopSettings(input: unknown): DesktopSettings {
|
||||
result.releaseChannel = releaseChannel;
|
||||
}
|
||||
|
||||
if (isRecord(input.notifications)) {
|
||||
const playSound = coerceBoolean(input.notifications.playSound);
|
||||
if (playSound !== null) {
|
||||
result.notifications.playSound = playSound;
|
||||
}
|
||||
}
|
||||
|
||||
if (isRecord(input.daemon)) {
|
||||
const manageBuiltInDaemon = coerceBoolean(input.daemon.manageBuiltInDaemon);
|
||||
if (manageBuiltInDaemon !== null) {
|
||||
@@ -124,6 +140,13 @@ function coerceDesktopSettingsPatch(input: unknown): DesktopSettingsPatch {
|
||||
patch.releaseChannel = releaseChannel;
|
||||
}
|
||||
|
||||
if (isRecord(input.notifications)) {
|
||||
const playSound = coerceBoolean(input.notifications.playSound);
|
||||
if (playSound !== null) {
|
||||
patch.notifications = { playSound };
|
||||
}
|
||||
}
|
||||
|
||||
if (isRecord(input.daemon)) {
|
||||
const daemonPatch: Partial<DesktopSettings["daemon"]> = {};
|
||||
const manageBuiltInDaemon = coerceBoolean(input.daemon.manageBuiltInDaemon);
|
||||
@@ -169,6 +192,7 @@ function mergeDesktopSettings(
|
||||
): DesktopSettings {
|
||||
return {
|
||||
releaseChannel: patch.releaseChannel ?? current.releaseChannel,
|
||||
notifications: { ...current.notifications, ...patch.notifications },
|
||||
daemon: { ...current.daemon, ...patch.daemon },
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user