diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index aa491b0197..3f13d90162 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -201,7 +201,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -212,7 +212,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -1238,7 +1238,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" dependencies = [ "lazy_static", - "windows-sys 0.59.0", + "windows-sys 0.48.0", ] [[package]] @@ -1986,7 +1986,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -2314,7 +2314,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -3426,7 +3426,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.6.5", + "socket2 0.5.10", "system-configuration", "tokio", "tower-service", @@ -4781,7 +4781,7 @@ dependencies = [ "png 0.18.1", "serde", "thiserror 2.0.20", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -4905,7 +4905,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -5390,7 +5390,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.48.0", ] [[package]] @@ -6088,7 +6088,7 @@ dependencies = [ "quinn-udp", "rustc-hash", "rustls", - "socket2 0.6.5", + "socket2 0.5.10", "thiserror 2.0.20", "tokio", "tracing", @@ -6127,9 +6127,9 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.6.5", + "socket2 0.5.10", "tracing", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -6740,7 +6740,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -6799,7 +6799,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -7587,7 +7587,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -8325,7 +8325,7 @@ dependencies = [ [[package]] name = "tauri-plugin-notifications" version = "0.5.0" -source = "git+https://github.com/SableClient/tauri-plugin-notifications.git?rev=308a0e8f3bb9513c3a1fd431fce98312b52c1d40#308a0e8f3bb9513c3a1fd431fce98312b52c1d40" +source = "git+https://github.com/SableClient/tauri-plugin-notifications.git?rev=658491c87be246740216f97cc367e29e31e690af#658491c87be246740216f97cc367e29e31e690af" dependencies = [ "log", "notify-rust", @@ -8680,7 +8680,7 @@ dependencies = [ "getrandom 0.4.3", "once_cell", "rustix", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -9311,7 +9311,7 @@ dependencies = [ "png 0.18.1", "serde", "thiserror 2.0.20", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -9397,7 +9397,7 @@ checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" dependencies = [ "memoffset", "tempfile", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -10129,7 +10129,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.48.0", ] [[package]] diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index e092c92b1e..8a69b0004c 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -110,12 +110,12 @@ windows = { version = "0.62", features = [ tauri-plugin-single-instance = { version = "2.4.3", features = ["deep-link"] } [target.'cfg(any(windows, target_os = "linux"))'.dependencies] -tauri-plugin-notifications = { git = "https://github.com/SableClient/tauri-plugin-notifications.git", rev = "308a0e8f3bb9513c3a1fd431fce98312b52c1d40" } +tauri-plugin-notifications = { git = "https://github.com/SableClient/tauri-plugin-notifications.git", rev = "658491c87be246740216f97cc367e29e31e690af" } # default-features = false drops notify-rust so macOS uses the native # UNUserNotificationCenter backend (needs a signed .app to deliver). [target.'cfg(target_os = "macos")'.dependencies] -tauri-plugin-notifications = { git = "https://github.com/SableClient/tauri-plugin-notifications.git", rev = "308a0e8f3bb9513c3a1fd431fce98312b52c1d40", default-features = false } +tauri-plugin-notifications = { git = "https://github.com/SableClient/tauri-plugin-notifications.git", rev = "658491c87be246740216f97cc367e29e31e690af", default-features = false } [target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] tauri-plugin-updater = { version = "2", optional = true } @@ -139,7 +139,7 @@ libloading = "0.9" zbus = "5" [target.'cfg(any(target_os = "android", target_os = "ios"))'.dependencies] -tauri-plugin-notifications = { git = "https://github.com/SableClient/tauri-plugin-notifications.git", rev = "308a0e8f3bb9513c3a1fd431fce98312b52c1d40", features = [ +tauri-plugin-notifications = { git = "https://github.com/SableClient/tauri-plugin-notifications.git", rev = "658491c87be246740216f97cc367e29e31e690af", features = [ "push-notifications", ] } tauri-plugin-edge-to-edge = { git = "https://github.com/SableClient/tauri-plugin-edge-to-edge.git", rev = "33c6116c27be28c06df5a9d02231ecc5fdeb93c5" } diff --git a/src/app/features/settings/notifications/BatteryOptimization.test.tsx b/src/app/features/settings/notifications/BatteryOptimization.test.tsx new file mode 100644 index 0000000000..8650ddee24 --- /dev/null +++ b/src/app/features/settings/notifications/BatteryOptimization.test.tsx @@ -0,0 +1,67 @@ +import { render, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { BatteryOptimizationSetting } from './BatteryOptimization'; + +const { isIgnoringBatteryOptimizations, requestIgnoreBatteryOptimizations } = vi.hoisted(() => ({ + isIgnoringBatteryOptimizations: vi.fn<() => Promise>(), + requestIgnoreBatteryOptimizations: vi.fn<() => Promise>(), +})); + +vi.mock('./UnifiedPushNotifications', () => ({ + isIgnoringBatteryOptimizations, + requestIgnoreBatteryOptimizations, +})); + +describe('BatteryOptimizationSetting', () => { + beforeEach(() => { + vi.clearAllMocks(); + requestIgnoreBatteryOptimizations.mockResolvedValue(undefined); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('prompts when the embedded distributor runs without an exemption', async () => { + isIgnoringBatteryOptimizations.mockResolvedValue(false); + + render(); + + const allow = await screen.findByRole('button', { name: /allow/i }); + await userEvent.click(allow); + + expect(requestIgnoreBatteryOptimizations).toHaveBeenCalledOnce(); + }); + + it('stays hidden once the exemption is granted', async () => { + isIgnoringBatteryOptimizations.mockResolvedValue(true); + + render(); + + await waitFor(() => expect(isIgnoringBatteryOptimizations).toHaveBeenCalled()); + expect(screen.queryByRole('button', { name: /allow/i })).not.toBeInTheDocument(); + }); + + it('stays hidden for other distributors', async () => { + render(); + + await Promise.resolve(); + expect(isIgnoringBatteryOptimizations).not.toHaveBeenCalled(); + expect(screen.queryByRole('button', { name: /allow/i })).not.toBeInTheDocument(); + }); + + it('re-checks when the user comes back from the system dialog', async () => { + isIgnoringBatteryOptimizations.mockResolvedValue(false); + + render(); + await screen.findByRole('button', { name: /allow/i }); + + isIgnoringBatteryOptimizations.mockResolvedValue(true); + document.dispatchEvent(new Event('visibilitychange')); + + await waitFor(() => + expect(screen.queryByRole('button', { name: /allow/i })).not.toBeInTheDocument() + ); + }); +}); diff --git a/src/app/features/settings/notifications/BatteryOptimization.tsx b/src/app/features/settings/notifications/BatteryOptimization.tsx new file mode 100644 index 0000000000..1c4537653c --- /dev/null +++ b/src/app/features/settings/notifications/BatteryOptimization.tsx @@ -0,0 +1,70 @@ +import { useCallback, useEffect, useState } from 'react'; +import { color, Text } from 'folds'; +import { Button } from '$components/button'; +import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback'; +import { SettingTile } from '../../../components/setting-tile'; +import { + isIgnoringBatteryOptimizations, + requestIgnoreBatteryOptimizations, +} from './UnifiedPushNotifications'; + +type BatteryOptimizationSettingProps = { + active: boolean; +}; + +export function BatteryOptimizationSetting({ active }: BatteryOptimizationSettingProps) { + const [ignoring, setIgnoring] = useState(null); + + const refresh = useCallback(() => { + void isIgnoringBatteryOptimizations().then(setIgnoring); + }, []); + + const [requestState, requestExemption] = useAsyncCallback( + useCallback(async () => { + await requestIgnoreBatteryOptimizations(); + }, []) + ); + + useEffect(() => { + if (!active) return undefined; + + refresh(); + const onVisibilityChange = () => { + if (document.visibilityState === 'visible') refresh(); + }; + document.addEventListener('visibilitychange', onVisibilityChange); + + return () => { + document.removeEventListener('visibilitychange', onVisibilityChange); + }; + }, [active, refresh]); + + if (!active || ignoring !== false) return null; + + return ( + + Allow + + } + > + {requestState.status === AsyncStatus.Error && ( + +
+ Could not open the battery settings. Allow unrestricted battery use for Sable manually. +
+ )} +
+ ); +} diff --git a/src/app/features/settings/notifications/SystemNotification.tsx b/src/app/features/settings/notifications/SystemNotification.tsx index 54301c166f..a71ad7e181 100644 --- a/src/app/features/settings/notifications/SystemNotification.tsx +++ b/src/app/features/settings/notifications/SystemNotification.tsx @@ -37,6 +37,7 @@ import { enablePushNotifications, disablePushNotifications, } from './PushNotifications'; +import { BatteryOptimizationSetting } from './BatteryOptimization'; import { DeregisterAllPushersSetting } from './DeregisterPushNotifications'; import { disableNativePush, @@ -912,6 +913,9 @@ function BackgroundPushNotificationSetting() { )} + { + if (!isTauri()) return null; + try { + return await invoke(IS_IGNORING_BATTERY_OPTIMIZATIONS); + } catch { + return null; + } +} + +export async function requestIgnoreBatteryOptimizations(): Promise { + if (!isTauri()) return; + await invoke(REQUEST_IGNORE_BATTERY_OPTIMIZATIONS); +} export type PushDiagnostics = { counts: Record;