From b9b3a24d58fb9f24f3960b4e9e9f15ffa5aa124c Mon Sep 17 00:00:00 2001 From: Glen Stampoultzis Date: Sun, 9 Aug 2026 06:28:37 +1000 Subject: [PATCH] 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. --- packages/app/src/components/ui/alert.tsx | 7 +- .../desktop-notifications-section.tsx | 150 ++++++++++++++++++ .../components/desktop-permission-row.tsx | 40 ++--- .../desktop-permissions-section.tsx | 54 +------ .../permissions/use-desktop-permissions.ts | 40 +++-- .../src/desktop/settings/desktop-settings.ts | 19 +++ packages/app/src/hooks/use-settings/fakes.ts | 3 + .../src/hooks/use-settings/storage.test.ts | 1 + packages/app/src/i18n/resources.test.ts | 4 +- packages/app/src/i18n/resources/ar.ts | 19 ++- packages/app/src/i18n/resources/en.ts | 19 ++- packages/app/src/i18n/resources/es.ts | 19 ++- packages/app/src/i18n/resources/fr.ts | 19 ++- packages/app/src/i18n/resources/ja.ts | 19 ++- packages/app/src/i18n/resources/ko.ts | 19 ++- packages/app/src/i18n/resources/pt-BR.ts | 19 ++- packages/app/src/i18n/resources/ru.ts | 19 ++- packages/app/src/i18n/resources/zh-CN.ts | 19 ++- packages/app/src/screens/settings-screen.tsx | 10 ++ packages/app/src/utils/host-routes.test.ts | 4 + packages/app/src/utils/host-routes.ts | 1 + .../desktop/src/features/notifications.ts | 4 +- .../src/settings/desktop-settings.test.ts | 42 +++++ .../desktop/src/settings/desktop-settings.ts | 24 +++ 24 files changed, 451 insertions(+), 123 deletions(-) create mode 100644 packages/app/src/desktop/components/desktop-notifications-section.tsx diff --git a/packages/app/src/components/ui/alert.tsx b/packages/app/src/components/ui/alert.tsx index a672c1c89..ef0f3e248 100644 --- a/packages/app/src/components/ui/alert.tsx +++ b/packages/app/src/components/ui/alert.tsx @@ -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["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; diff --git a/packages/app/src/desktop/components/desktop-notifications-section.tsx b/packages/app/src/desktop/components/desktop-notifications-section.tsx new file mode 100644 index 000000000..a45d64744 --- /dev/null +++ b/packages/app/src/desktop/components/desktop-notifications-section.tsx @@ -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(() => , []); + const refreshButton = useMemo( + () => ( + + ), + [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 ( + + + + + + {t("settings.notifications.playSound")} + {t("settings.notifications.playSoundHint")} + + + + + + {t("settings.notifications.test")} + + {notificationsGranted + ? t("settings.notifications.testHint") + : t("settings.notifications.permissionRequired")} + + + + + + {testNotificationState.status === "success" ? ( + + ) : null} + {testNotificationState.status === "error" ? ( + + ) : null} + + ); +} diff --git a/packages/app/src/desktop/components/desktop-permission-row.tsx b/packages/app/src/desktop/components/desktop-permission-row.tsx index f97050b7d..950d9c6c1 100644 --- a/packages/app/src/desktop/components/desktop-permission-row.tsx +++ b/packages/app/src/desktop/components/desktop-permission-row.tsx @@ -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({ {isGranted ? ( - - - - {labels.granted} - - {extraActionLabel && onExtraAction ? ( - - ) : null} + + + {labels.granted} ) : (