Skip to content

Conversation

@MohammadShujaullah
Copy link

@MohammadShujaullah MohammadShujaullah commented Jan 7, 2026

Fixes#5864

What this PR does-----------------------------------

  • Exposes system power state to the UI layer
  • Disables high power consumption effects when running on battery
  • Automatically restores effects when plugged in

#Why------------------------------------------------
This improves battery life and aligns the UI behavior with system power conditions.

How it works---------------------------

  • Uses Electron powerMonitor to detect power state
  • Exposes state securely via preload API
  • UI reacts to battery state and reduces animations/effects

#Checklist-------------------------

  • Tested locally
  • No breaking changes
  • Works on Windows

Summary by CodeRabbit

Release Notes

  • New Features
    • Automatic animation reduction when device is running on battery power to help conserve energy
    • Enhanced accessibility support for users who prefer reduced motion settings
    • System now intelligently detects device power state and motion preferences, disabling animations when appropriate to improve battery life and user experience

✏️ Tip: You can customize this high-level summary in your review settings.

@CLAassistant
Copy link

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@github-actions github-actions bot added app:electron Related to electron app mod:dev test Related to test cases app:core labels Jan 7, 2026
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 7, 2026

📝 Walkthrough

Walkthrough

This change introduces power state monitoring to the Electron app. When the device runs on battery, the system automatically reduces animations and transitions by applying a reduce-motion CSS class. The implementation combines device battery state detection with user reduce-motion preferences to minimize motion effects during low-power scenarios.

Changes

Cohort / File(s) Summary
Electron Main Process Power State
packages/frontend/apps/electron/src/main/index.ts
Imports powerMonitor from Electron; initializes battery state tracking via powerMonitor.isOnBatteryPower(); registers listeners for battery/AC transitions; exposes new IPC channel get-power-state to return current power state.
Frontend Power State Detection
packages/frontend/apps/electron-renderer/src/app/hooks/use-power-state.ts
New React hook that queries battery state on mount via window.__apis.getPowerState() and returns boolean indicating if device is on battery.
Power State Sync & Integration
packages/frontend/apps/electron-renderer/src/app/power-state-sync.ts, app.tsx
New component DesktopPowerStateSync that monitors battery state and user reduce-motion preferences, conditionally applying reduce-motion CSS class to document root; injected into app render tree.
Reduce Motion Styling
packages/frontend/apps/electron-renderer/src/app/global.css
New CSS rule targeting html.reduce-motion that overrides animation and transition durations to minimal values, limiting iterations and enabling auto scroll behavior.
Package Cleanup
packages/frontend/apps/ios/App/App.xcworkspace/xcshareddata/swiftpm/Package.resolved
Deleted Swift Package Manager dependency lock file.

Sequence Diagram(s)

sequenceDiagram
    actor System as System (Battery)
    participant Main as Electron Main
    participant Renderer as React Renderer
    participant DOM as Document DOM

    System->>Main: Power state changes (on-battery/on-ac)
    Main->>Main: Update isOnBatteryPower state
    Renderer->>Main: Query get-power-state via IPC (on mount)
    Main-->>Renderer: Return battery status
    Renderer->>Renderer: usePowerState hook updates state
    Note over Renderer: Check prefers-reduced-motion<br/>media query
    Renderer->>Renderer: DesktopPowerStateSync calculates<br/>reduce-motion needed
    alt Should Enable Reduce Motion
        Renderer->>DOM: Add 'reduce-motion' class
        DOM->>DOM: Apply reduce-motion CSS rules
    else Should Allow Animations
        Renderer->>DOM: Remove 'reduce-motion' class
        DOM->>DOM: Restore normal animations
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 Hops through the code with batteries low,
A rabbit ensures motion won't steal the show,
Power state dancing, CSS in place,
No jitter, no flicker—just smooth, steady grace!

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: disable high power consumption effects on battery' clearly and directly describes the main objective of the changeset—detecting battery state and disabling animations/effects to conserve power. It matches the core functionality added across all modified files.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In @packages/frontend/apps/electron/src/main/index.ts:
- Around line 104-114: The powerMonitor listeners and the initial
isOnBatteryPower read are being registered at module scope before Electron is
ready; wrap the registration and the isOnBatteryPower assignment in
app.whenReady().then(...) so that powerMonitor.on('on-battery', ...),
powerMonitor.on('on-ac', ...), and the isOnBatteryPower =
powerMonitor.isOnBatteryPower() call execute only after app.whenReady()
resolves, keeping the same handler logic and logger.info messages.
🧹 Nitpick comments (1)
packages/frontend/apps/electron-renderer/src/app/global.css (1)

27-35: Consider animation-timing-function and review !important usage.

The CSS effectively disables animations, but consider:

  1. Missing property: animation-timing-function is not overridden. While animation-duration: 0.01ms makes this mostly moot, explicitly setting it to linear would be more thorough.

  2. !important usage: The liberal use of !important may be necessary to override inline styles or third-party libraries, but it's worth confirming this is required. If possible, rely on specificity instead.

♻️ Suggested refinement
 /* Disable animations when on battery power */
 html.reduce-motion *,
 html.reduce-motion *::before,
 html.reduce-motion *::after {
   animation-duration: 0.01ms !important;
+  animation-timing-function: linear !important;
   animation-iteration-count: 1 !important;
   transition-duration: 0.01ms !important;
   scroll-behavior: auto !important;
 }
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between e4dc82e and ef52886.

⛔ Files ignored due to path filters (7)
  • .yarn/releases/yarn-4.12.0.cjs is excluded by !**/.yarn/**
  • blocksuite/integration-test/src/__tests__/edgeless/__screenshots__/layer.spec.ts/if-the-topmost-layer-is-canvas-layer--the-length-of-canvasLayers-array-should-equal-to-the-counts-of-canvas-layers-1.png is excluded by !**/*.png
  • blocksuite/playground/apps/starter/data/snapshots/affine-default.zip is excluded by !**/*.zip
  • packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Controller/AttachmentManagementController/AttachmentIcon.xcassets/FileAttachment_json.imageset/AiFileClip attachment icon.pdf is excluded by !**/*.pdf
  • packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Controller/AttachmentManagementController/AttachmentIcon.xcassets/FileAttachment_md.imageset/AiFileClip attachment icon.pdf is excluded by !**/*.pdf
  • packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Controller/AttachmentManagementController/AttachmentIcon.xcassets/FileAttachment_pdf.imageset/AiFileClip attachment icon.pdf is excluded by !**/*.pdf
  • packages/frontend/apps/ios/App/Packages/Intelligents/Sources/Intelligents/Interface/Controller/AttachmentManagementController/AttachmentIcon.xcassets/FileAttachment_txt.imageset/AiFileClip attachment icon.pdf is excluded by !**/*.pdf
📒 Files selected for processing (20)
  • .devcontainer/setup-user.sh
  • .husky/pre-commit
  • packages/common/y-octo/node/scripts/run-test.mts
  • packages/frontend/apps/android/App/gradlew
  • packages/frontend/apps/electron-renderer/src/app/app.tsx
  • packages/frontend/apps/electron-renderer/src/app/global.css
  • packages/frontend/apps/electron-renderer/src/app/hooks/use-power-state.ts
  • packages/frontend/apps/electron-renderer/src/app/power-state-sync.ts
  • packages/frontend/apps/electron/scripts/generate-assets.ts
  • packages/frontend/apps/electron/src/main/index.ts
  • packages/frontend/apps/ios/App/App.xcworkspace/xcshareddata/swiftpm/Package.resolved
  • packages/frontend/apps/ios/apollo-codegen-chore.sh
  • packages/frontend/apps/ios/setup.sh
  • packages/frontend/apps/ios/update_code_sign_identity.sh
  • packages/frontend/core/public/manifest.json
  • scripts/cleanup-canary-releases.sh
  • scripts/set-version.sh
  • tools/cli/bin/cli.js
  • tools/cli/bin/runner.js
  • tools/cli/src/init.ts
💤 Files with no reviewable changes (1)
  • packages/frontend/apps/ios/App/App.xcworkspace/xcshareddata/swiftpm/Package.resolved
🧰 Additional context used
🧬 Code graph analysis (2)
packages/frontend/apps/electron-renderer/src/app/power-state-sync.ts (1)
packages/frontend/apps/electron-renderer/src/app/hooks/use-power-state.ts (1)
  • usePowerState (3-11)
packages/frontend/apps/electron-renderer/src/app/app.tsx (1)
packages/frontend/apps/electron-renderer/src/app/power-state-sync.ts (1)
  • DesktopPowerStateSync (5-38)
🔇 Additional comments (2)
packages/frontend/apps/electron-renderer/src/app/app.tsx (1)

14-14: LGTM: Component integration follows established patterns.

The placement and usage of DesktopPowerStateSync is consistent with other sync components (DesktopThemeSync, DesktopLanguageSync) in the component tree.

Also applies to: 52-52

packages/frontend/apps/electron-renderer/src/app/power-state-sync.ts (1)

5-38: Component logic is sound, but depends on fixing the power state hook.

The synchronization logic correctly combines battery state and user preferences:

  • Animations are enabled only when NOT on battery AND user does NOT prefer reduced motion
  • The reduce-motion class is applied when either condition requires it
  • Cross-browser compatibility for media query listeners is well-handled

However, this component's effectiveness is blocked by the critical issue in usePowerState (packages/frontend/apps/electron-renderer/src/app/hooks/use-power-state.ts), which doesn't update when power state changes at runtime.

Optional optimization:

The media query could be memoized to avoid recreating it on every effect run:

♻️ Minor optimization
+import { useEffect, useMemo, useState } from 'react';

-import { usePowerState } from './hooks/use-power-state';
+import { usePowerState } from './hooks/use-power-state';

 export const DesktopPowerStateSync = () => {
   const onBattery = usePowerState();
+  const mediaQuery = useMemo(
+    () => window.matchMedia('(prefers-reduced-motion: reduce)'),
+    []
+  );
   const [prefersReducedMotion, setPrefersReducedMotion] = useState(
-    () => window.matchMedia('(prefers-reduced-motion: reduce)').matches
+    () => mediaQuery.matches
   );

   useEffect(() => {
-    const mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)');
     const handleChange = (e: MediaQueryListEvent) => {
       setPrefersReducedMotion(e.matches);
     };

     // Modern browsers
     if (mediaQuery.addEventListener) {
       mediaQuery.addEventListener('change', handleChange);
       return () => mediaQuery.removeEventListener('change', handleChange);
     }
     // Fallback for older browsers
     else if (mediaQuery.addListener) {
       mediaQuery.addListener(handleChange);
       return () => mediaQuery.removeListener(handleChange);
     }
-  }, []);
+  }, [mediaQuery]);

   useEffect(() => {
     const enableAnimations = !onBattery && !prefersReducedMotion;
     document.documentElement.classList.toggle(
       'reduce-motion',
       !enableAnimations
     );
   }, [onBattery, prefersReducedMotion]);

   return null;
 };

Comment on lines +3 to +11
export function usePowerState() {
const [onBattery, setOnBattery] = useState(false);

useEffect(() => {
window.__apis.getPowerState().then(setOnBattery);
}, []);

return onBattery;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: Hook doesn't react to power state changes.

This hook queries the power state only once on mount and never updates. Combined with the main process not broadcasting power state changes, this means:

  1. If the user unplugs/plugs in their device after mount, the UI won't respond
  2. The reduce-motion behavior will remain stale until the app restarts
  3. The battery-saving feature is effectively broken for runtime power changes

Additionally, there's no error handling for the IPC call, which could cause the component to silently fail.

🔧 Proposed fix: Add event listener for power state updates
 import { useEffect, useState } from 'react';

 export function usePowerState() {
   const [onBattery, setOnBattery] = useState(false);

   useEffect(() => {
-    window.__apis.getPowerState().then(setOnBattery);
-  }, []);
+    // Get initial state
+    window.__apis.getPowerState()
+      .then(setOnBattery)
+      .catch(err => {
+        console.error('Failed to get power state:', err);
+      });
+
+    // Listen for power state changes
+    const handlePowerStateChange = (_event: any, onBattery: boolean) => {
+      setOnBattery(onBattery);
+    };
+
+    window.electron?.ipcRenderer.on('power-state-changed', handlePowerStateChange);
+
+    return () => {
+      window.electron?.ipcRenderer.removeListener('power-state-changed', handlePowerStateChange);
+    };
+  }, []);

   return onBattery;
 }

Note: This requires the main process changes suggested in packages/frontend/apps/electron/src/main/index.ts to broadcast power state changes.

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +104 to +114
powerMonitor.on('on-battery', () => {
isOnBatteryPower = true;
logger.info('Running on battery');
});

powerMonitor.on('on-ac', () => {
isOnBatteryPower = false;
logger.info('Running on AC power');
});

isOnBatteryPower = powerMonitor.isOnBatteryPower();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

Electron powerMonitor API requirements app ready event listeners

💡 Result:

Short answer

  • powerMonitor is a main-process API and cannot be required or used until app has emitted the ready event. Use app.on('ready', …) or app.whenReady() (or check app.isReady()) before attaching listeners. [1][2]

Example

  • Recommended pattern:
    const { app, powerMonitor } = require('electron')
    app.whenReady().then(() => {
    powerMonitor.on('suspend', () => { console.log('suspend') })
    powerMonitor.on('resume', () => { console.log('resume') })
    })

Notes

  • powerMonitor emits events like 'suspend', 'resume', 'on-ac', 'on-battery' (Windows) and 'shutdown' (macOS/Linux). [1]
  • Put all powerMonitor usage in the main process after ready to avoid runtime errors. [1][2]

Sources
[1] Electron powerMonitor docs.
[2] Electron app lifecycle (ready / whenReady / isReady).


🏁 Script executed:

# First, let's examine the actual file to understand the code context
head -120 packages/frontend/apps/electron/src/main/index.ts | tail -50

Repository: toeverything/AFFiNE

Length of output: 1126


🏁 Script executed:

# Get more context to see app.whenReady() calls and overall structure
cat -n packages/frontend/apps/electron/src/main/index.ts | head -130

Repository: toeverything/AFFiNE

Length of output: 5290


🏁 Script executed:

# Check if app.whenReady() is used elsewhere in the file
rg "app\.whenReady" packages/frontend/apps/electron/src/main/index.ts

Repository: toeverything/AFFiNE

Length of output: 45


Wrap powerMonitor listeners in app.whenReady() before registration.

The Electron powerMonitor API cannot be used until the app has emitted the ready event. Currently, the event listeners at lines 104-114 are registered at module scope during initialization, which executes before app.whenReady() resolves. This violates Electron's API requirements and will cause runtime errors.

Move these listeners into an app.whenReady().then() block:

Example fix:
app.whenReady().then(() => {
  powerMonitor.on('on-battery', () => {
    isOnBatteryPower = true;
    logger.info('Running on battery');
  });

  powerMonitor.on('on-ac', () => {
    isOnBatteryPower = false;
    logger.info('Running on AC power');
  });

  isOnBatteryPower = powerMonitor.isOnBatteryPower();
});
🤖 Prompt for AI Agents
In @packages/frontend/apps/electron/src/main/index.ts around lines 104 - 114,
The powerMonitor listeners and the initial isOnBatteryPower read are being
registered at module scope before Electron is ready; wrap the registration and
the isOnBatteryPower assignment in app.whenReady().then(...) so that
powerMonitor.on('on-battery', ...), powerMonitor.on('on-ac', ...), and the
isOnBatteryPower = powerMonitor.isOnBatteryPower() call execute only after
app.whenReady() resolves, keeping the same handler logic and logger.info
messages.

Comment on lines +116 to +118
ipcMain.handle('get-power-state', () => {
return isOnBatteryPower;
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: Power state changes are not propagated to the renderer.

The IPC handler only returns the current state when queried, but there's no mechanism to notify the renderer when power state changes. This means:

  1. The renderer's usePowerState hook queries once on mount and never updates
  2. If a user unplugs their device after the app loads, the UI will continue showing AC-powered behavior
  3. The on-battery and on-ac event handlers update isOnBatteryPower but don't send events to the renderer
💡 Proposed fix: Add event broadcasting to renderer
+/**
+ * Broadcast power state changes to all renderer windows
+ */
+function broadcastPowerStateChange(onBattery: boolean) {
+  const { BrowserWindow } = require('electron');
+  BrowserWindow.getAllWindows().forEach(window => {
+    window.webContents.send('power-state-changed', onBattery);
+  });
+}
+
 powerMonitor.on('on-battery', () => {
   isOnBatteryPower = true;
   logger.info('Running on battery');
+  broadcastPowerStateChange(true);
 });

 powerMonitor.on('on-ac', () => {
   isOnBatteryPower = false;
   logger.info('Running on AC power');
+  broadcastPowerStateChange(false);
 });

Then update the renderer's usePowerState hook to listen for these events.

Committable suggestion skipped: line range outside the PR's diff.

@nx-cloud
Copy link

nx-cloud bot commented Jan 8, 2026

View your CI Pipeline Execution ↗ for commit ef52886

Command Status Duration Result
nx build @affine/server-native -- --target armv... ✅ Succeeded 1m 1s View ↗
nx build @affine/server-native -- --target aarc... ✅ Succeeded 1m 3s View ↗
nx build @affine/server-native -- --target x86_... ✅ Succeeded 1m 1s View ↗

☁️ Nx Cloud last updated this comment at 2026-01-09 09:27:59 UTC

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

app:core app:electron Related to electron app mod:dev test Related to test cases

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

2 participants