Mobile, Desktop: Resolves #9481: Start sync when app opens or resumes (#14574)

This commit is contained in:
Yousef Genedy
2026-03-10 13:27:46 +02:00
committed by GitHub
parent 7214823c74
commit 2a681008dd
3 changed files with 52 additions and 4 deletions

View File

@@ -6,7 +6,7 @@ const shim: typeof ShimType = require('@joplin/lib/shim').default;
import { isCallbackUrl } from '@joplin/lib/callbackUrlUtils';
import { FileLocker } from '@joplin/utils/fs';
import { IpcMessageHandler, IpcServer, Message, newHttpError, sendMessage, SendMessageOptions, startServer, stopServer } from '@joplin/utils/ipc';
import { BrowserWindow, Tray, WebContents, screen, App, nativeTheme } from 'electron';
import { BrowserWindow, Tray, WebContents, screen, App, nativeTheme, powerMonitor } from 'electron';
import bridge from './bridge';
import * as url from 'url';
const path = require('path');
@@ -401,6 +401,15 @@ export default class ElectronAppWrapper {
};
addWindowEventHandlers(this.win_.webContents);
// BrowserWindow 'focus' fires when the OS gives focus to the application window
// (i.e. coming from another app or from the taskbar), not on intra-app focus switches.
// We use a dedicated IPC channel so the renderer can trigger an immediate sync on
// OS-level focus gain without conflating it with the 'window-focused' channel that
// handles Joplin-internal window routing.
this.win_.on('focus', () => {
this.win_?.webContents.send('main-window-focused');
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
this.win_.on('close', (event: any) => {
// If it's on macOS, the app is completely closed only if the user chooses to close the app (willQuitApp_ will be true)
@@ -892,6 +901,11 @@ export default class ElectronAppWrapper {
event.preventDefault();
void this.openCallbackUrl(url);
});
// When the OS wakes from sleep, notify the renderer so it can trigger an immediate sync.
powerMonitor.on('resume', () => {
this.win_?.webContents.send('system-resumed');
});
}
public async openCallbackUrl(url: string) {

View File

@@ -733,6 +733,23 @@ class Application extends BaseApplication {
});
}
});
// Trigger an immediate sync when the main window gains OS-level focus (i.e. the user
// switches back to Joplin from another application) or when the system wakes from sleep.
// A 30-second cool-down prevents duplicate syncs during rapid focus-in/focus-out cycles.
const minResumeSyncIntervalMs = 30_000;
let lastFocusSyncTime = 0;
const scheduleResumeSync = () => {
const now = Date.now();
if (now - lastFocusSyncTime > minResumeSyncIntervalMs) {
lastFocusSyncTime = now;
void reg.scheduleSync(0);
}
};
ipcRenderer.on('main-window-focused', scheduleResumeSync);
ipcRenderer.on('system-resumed', scheduleResumeSync);
});
addTask('app/initPluginService', () => this.initPluginService());

View File

@@ -19,7 +19,7 @@ import SyncTargetJoplinServer from '@joplin/lib/SyncTargetJoplinServer';
import SyncTargetJoplinCloud from '@joplin/lib/SyncTargetJoplinCloud';
import SyncTargetOneDrive from '@joplin/lib/SyncTargetOneDrive';
import { Keyboard, BackHandler, Animated, StatusBar, Platform, Dimensions } from 'react-native';
import { AppState as RNAppState, EmitterSubscription, View, Text, Linking, NativeEventSubscription, Appearance, ActivityIndicator } from 'react-native';
import { AppState as RNAppState, AppStateStatus, EmitterSubscription, View, Text, Linking, NativeEventSubscription, Appearance, ActivityIndicator } from 'react-native';
import getResponsiveValue from './components/getResponsiveValue';
import NetInfo, { NetInfoSubscription } from '@react-native-community/netinfo';
const DropdownAlert = require('react-native-dropdownalert').default;
@@ -295,7 +295,8 @@ class AppComponent extends React.Component<AppComponentProps, AppComponentState>
private unsubscribeScreenWidthChangeHandler_: EmitterSubscription|undefined;
private unsubscribeNetInfoHandler_: NetInfoSubscription|undefined;
private unsubscribeNewShareListener_: UnsubscribeShareListener|undefined;
private onAppStateChange_: ()=> void;
private onAppStateChange_: (nextAppState: AppStateStatus)=> void;
private lastResumeSyncTime_ = 0;
private backButtonHandler_: BackButtonHandler;
private handleNewShare_: ()=> void;
private handleOpenURL_: (event: unknown)=> void;
@@ -315,8 +316,24 @@ class AppComponent extends React.Component<AppComponentProps, AppComponentState>
return this.backButtonHandler();
};
this.onAppStateChange_ = () => {
this.onAppStateChange_ = (nextAppState: AppStateStatus) => {
PoorManIntervals.update();
// Trigger sync immediately when the app becomes active (resume from background/lock screen).
// Only run when the app becomes active, with a 30-second minimum interval
// prevent sync spam on rapid lock/unlock cycles.
const minResumeSyncIntervalMs = 30_000;
if (nextAppState === 'active') {
const elapsed = Date.now() - this.lastResumeSyncTime_;
if (elapsed >= minResumeSyncIntervalMs) {
logger.info(`onAppStateChange_: App became active - scheduling immediate sync (elapsed since last resume sync: ${elapsed}ms)`);
this.lastResumeSyncTime_ = Date.now();
void reg.scheduleSync(0, null, true);
} else {
logger.info(`onAppStateChange_: App became active but skipping sync - minimum interval not reached (${elapsed}ms < ${minResumeSyncIntervalMs}ms)`);
}
}
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied