Repository: laurent22/joplin (TypeScript)

Issue:
Joplin window goes blank after moving window and then minimizing
### Operating system

Windows

### Joplin version

3.3.9

### Desktop version info

Joplin 3.3.9 (prod, win32)

Device: win32, x64
Client ID: acf44d67875e4c478d29d7d4c738fc72
Sync Version: 3
Profile Version: 47
Keychain Supported: Yes
Alternative instance ID: -

Revision: 5d0fa75

Backup: 1.4.3
Journal: 2.0.0
Note Tabs: 1.4.0

### Current behaviour

Operating System: Windows 11 24H2

Steps to Re-create:

1. Open Joplin
2. Minimize and un-minimize joplin, you should see that it works as expected
3. Move the window slightly to the left or right
4. Minimize joplin
5. Unminimize joplin and the window will be blank

https://github.com/user-attachments/assets/54c3bb9e-d6de-4e80-a766-e38d2c56c4e3

### Expected behaviour

The UI should always appear as expected when un-minimizing the window

### Logs

_No response_


Discussion:
Same here. But I have it directly after starting Joplin and it's a black screen for me.
I haven't tried opening from the tray (in my case, application tray setting was disabled), but this might be related to (or even same issue as): https://github.com/laurent22/joplin/issues/12077 
I have the same issue, I just upgraded to 3.3.9. I've had it with the latest stable release before 3.3.9.

Just to confirm: I still have this issue with the latest release 3.3.12. In my case I don't see the menu either.
potentially related electron issue: https://github.com/electron/electron/issues/42378 
~~for a temporary workaround, launching joplin with **--disable-gpu** to disable hardware acceleration seems to avoid the issue for me~~

EDIT: actually, it appears that it still happens
updated description and added steps to reliably re-create behavior
I’m experiencing the same bug in Joplin. I’ve tried everything, including **disabling all plugins** and **reinstalling**. It’s completely disrupted my workflow。
I’m now searching for replacements。🤣
@hlw8895 
For me it's okay when I minimize Joplin again and restore it. At least on Windows.

@Tragen 
Try `3 Move the window slightly to the left or right, ` as mentioned **in the video above**.  may blank？😥
For now, I’ve maximized the window and am avoiding dragging it（not move joplin whidows）, and this bug hasn’t reappeared. 😌
yeah the issue doesn't seem to appear as long as you keep the window maximized

the other alternative is enabling system tray icon and always closing to tray (the issue only happens when restoring from tray if the joplin window is open and minimized, it does not seem to happen if joplin is completely closed to tray)
Think I have a workaround on Windows:

When it goes blank, maximize and then un-maximize it. The text re-appears
For me it's still not fixed in 3.4.2
When I start Joplin and open the window on Windows, it's blank.
I still have to minimize Joplin one time until it is usable

It seems to be fixed for me in 3.4.2, I no longer see the window going blank when using the steps from my original post (moving the window before minimizing it)

I never had the issue where Joplin was blank on a cold launch though, for me the issue only ever presented after minimizing/unminimizing the window



Patch:
diff --git a/packages/app-desktop/app.reducer.ts b/packages/app-desktop/app.reducer.ts
index 04cf5ac11ef..1b621619bc1 100644
--- a/packages/app-desktop/app.reducer.ts
+++ b/packages/app-desktop/app.reducer.ts
@@ -54,8 +54,6 @@ export interface AppState extends State, AppWindowState {
 	route: AppStateRoute;
 	// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
 	navHistory: any[];
-	// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
-	windowContentSize: any;
 	watchedNoteFiles: string[];
 	lastEditorScrollPercents: EditorScrollPercents;
 	focusedField: string;
@@ -81,7 +79,7 @@ export const createAppDefaultWindowState = (): AppWindowState => {
 };
 
 // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
-export function createAppDefaultState(windowContentSize: any, resourceEditWatcherDefaultState: any): AppState {
+export function createAppDefaultState(resourceEditWatcherDefaultState: any): AppState {
 	return {
 		...defaultState,
 		...createAppDefaultWindowState(),
@@ -91,7 +89,6 @@ export function createAppDefaultState(windowContentSize: any, resourceEditWatche
 			props: {},
 		},
 		navHistory: [],
-		windowContentSize, // bridge().windowContentSize(),
 		watchedNoteFiles: [],
 		lastEditorScrollPercents: {},
 		visibleDialogs: {}, // empty object if no dialog is visible. Otherwise contains the list of visible dialogs.
@@ -166,12 +163,6 @@ export default function(state: AppState, action: any) {
 			}
 			break;
 
-		case 'WINDOW_CONTENT_SIZE_SET':
-
-			newState = { ...state };
-			newState.windowContentSize = action.size;
-			break;
-
 		case 'NOTE_VISIBLE_PANES_TOGGLE':
 
 			{
diff --git a/packages/app-desktop/app.ts b/packages/app-desktop/app.ts
index d2c541f823f..17efcf03d6a 100644
--- a/packages/app-desktop/app.ts
+++ b/packages/app-desktop/app.ts
@@ -65,10 +65,7 @@ const pluginClasses = [
 	require('./plugins/GotoAnything').default,
 ];
 
-const appDefaultState = createAppDefaultState(
-	bridge().windowContentSize(),
-	resourceEditWatcherDefaultState,
-);
+const appDefaultState = createAppDefaultState(resourceEditWatcherDefaultState);
 
 class Application extends BaseApplication {
 
diff --git a/packages/app-desktop/bridge.ts b/packages/app-desktop/bridge.ts
index a04988d725d..ea5a2059d78 100644
--- a/packages/app-desktop/bridge.ts
+++ b/packages/app-desktop/bridge.ts
@@ -313,13 +313,6 @@ export class Bridge {
 		return new BrowserWindow(options);
 	}
 
-	// Note: This provides the size of the main window. Prefer CSS where possible.
-	public windowContentSize() {
-		if (!this.mainWindow()) return { width: 0, height: 0 };
-		const s = this.mainWindow().getContentSize();
-		return { width: s[0], height: s[1] };
-	}
-
 	public windowSetSize(width: number, height: number) {
 		if (!this.mainWindow()) return;
 		return this.mainWindow().setSize(width, height);
diff --git a/packages/app-desktop/gui/Navigator.tsx b/packages/app-desktop/gui/Navigator.tsx
index 4aa3acf9d7c..ddaff17d856 100644
--- a/packages/app-desktop/gui/Navigator.tsx
+++ b/packages/app-desktop/gui/Navigator.tsx
@@ -3,7 +3,7 @@ import { connect } from 'react-redux';
 import Setting from '@joplin/lib/models/Setting';
 import { AppState, AppStateRoute } from '../app.reducer';
 import bridge from '../services/bridge';
-import { useContext, useEffect, useMemo, useRef } from 'react';
+import { useContext, useEffect, useMemo, useRef, useState } from 'react';
 import { WindowIdContext } from './NewWindowOrIFrame';
 
 // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Partial refactor of code from before rule was applied
@@ -55,26 +55,44 @@ const useWindowRefocusManager = (route: AppStateRoute) => {
 	}, [routeName, windowId]);
 };
 
+const useContainerSize = (container: HTMLElement|null) => {
+	const [size, setSize] = useState({ width: container?.clientWidth, height: container?.clientHeight });
+
+	useEffect(() => {
+		if (!container) return () => {};
+
+		const observer = new ResizeObserver(() => {
+			setSize({
+				width: container.clientWidth,
+				height: container.clientHeight,
+			});
+		});
+		observer.observe(container);
+		return () => {
+			observer.disconnect();
+		};
+	}, [container]);
+
+	return size;
+};
+
 const NavigatorComponent: React.FC<Props> = props => {
 	const route = props.route;
 	const screenInfo = props.screens[route?.routeName];
+	const [container, setContainer] = useState<HTMLElement|null>(null);
 
 	useWindowTitleManager(screenInfo);
 	useWindowRefocusManager(route);
+	const size = useContainerSize(container);
 
 	if (!route) throw new Error('Route must not be null');
 
 	const screenProps = route.props ? route.props : {};
 	const Screen = screenInfo.screen;
 
-	const screenStyle = {
-		width: props.style.width,
-		height: props.style.height,
-	};
-
 	return (
-		<div style={props.style} className={props.className}>
-			<Screen style={screenStyle} {...screenProps} />
+		<div ref={setContainer} style={props.style} className={props.className}>
+			<Screen style={size} {...screenProps} />
 		</div>
 	);
 };
diff --git a/packages/app-desktop/gui/Root.tsx b/packages/app-desktop/gui/Root.tsx
index a58d8735039..0bbdb8c1181 100644
--- a/packages/app-desktop/gui/Root.tsx
+++ b/packages/app-desktop/gui/Root.tsx
@@ -8,13 +8,11 @@ import OneDriveLoginScreen from './OneDriveLoginScreen';
 import DropboxLoginScreen from './DropboxLoginScreen';
 import ErrorBoundary from './ErrorBoundary';
 import { themeStyle } from '@joplin/lib/theme';
-import { Size } from './ResizableLayout/utils/types';
 import MenuBar from './MenuBar';
 import { _ } from '@joplin/lib/locale';
 const { createRoot } = require('react-dom/client');
 const { connect, Provider } = require('react-redux');
 import Setting from '@joplin/lib/models/Setting';
-import shim from '@joplin/lib/shim';
 import ClipperServer from '@joplin/lib/ClipperServer';
 import DialogTitle from './DialogTitle';
 import DialogButtonRow, { ButtonSpec, ClickEvent, ClickEventHandler } from './DialogButtonRow';
@@ -28,7 +26,6 @@ import JoplinCloudLoginScreen from './JoplinCloudLoginScreen';
 import InteropService from '@joplin/lib/services/interop/InteropService';
 import WindowCommandsAndDialogs from './WindowCommandsAndDialogs/WindowCommandsAndDialogs';
 import { defaultWindowId, stateUtils, WindowState } from '@joplin/lib/reducer';
-import bridge from '../services/bridge';
 import EditorWindow from './NoteEditor/EditorWindow';
 import SsoLoginScreen from './SsoLoginScreen/SsoLoginScreen';
 import SamlShared from '@joplin/lib/components/shared/SamlShared';
@@ -41,7 +38,6 @@ interface Props {
 	profileConfigCurrentProfileId: string;
 	// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
 	dispatch: Function;
-	size: Size;
 	zoomFactor: number;
 	needApiAuth: boolean;
 	dialogs: AppStateDialog[];
@@ -62,31 +58,9 @@ const GlobalStyle = createGlobalStyle`
 	}
 `;
 
-// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
-let wcsTimeoutId_: any = null;
+const navigatorStyle = { width: '100vw', height: '100vh' };
 
 async function initialize() {
-	bridge().activeWindow().on('resize', () => {
-		if (wcsTimeoutId_) shim.clearTimeout(wcsTimeoutId_);
-
-		wcsTimeoutId_ = shim.setTimeout(() => {
-			store.dispatch({
-				type: 'WINDOW_CONTENT_SIZE_SET',
-				size: bridge().windowContentSize(),
-			});
-			wcsTimeoutId_ = null;
-		}, 10);
-	});
-
-	// Need to dispatch this to make sure the components are
-	// displayed at the right size. The windowContentSize is
-	// also set in the store default state, but at that point
-	// the window might not be at its final size.
-	store.dispatch({
-		type: 'WINDOW_CONTENT_SIZE_SET',
-		size: bridge().windowContentSize(),
-	});
-
 	store.dispatch({
 		type: 'EDITOR_CODE_VIEW_CHANGE',
 		value: Setting.value('editor.codeView'),
@@ -180,11 +154,6 @@ class RootComponent extends React.Component<Props, any> {
 	}
 
 	public render() {
-		const navigatorStyle = {
-			width: this.props.size.width / this.props.zoomFactor,
-			height: this.props.size.height / this.props.zoomFactor,
-		};
-
 		const theme = themeStyle(this.props.themeId);
 
 		const screens = {
@@ -219,7 +188,6 @@ class RootComponent extends React.Component<Props, any> {
 
 const mapStateToProps = (state: AppState) => {
 	return {
-		size: state.windowContentSize,
 		zoomFactor: state.settings.windowContentZoomFactor / 100,
 		appState: state.appState,
 		themeId: state.settings.theme,


Test patch:
diff --git a/packages/app-desktop/app.reducer.test.ts b/packages/app-desktop/app.reducer.test.ts
index 8f6f87e8bcc..4e6626a9e84 100644
--- a/packages/app-desktop/app.reducer.test.ts
+++ b/packages/app-desktop/app.reducer.test.ts
@@ -4,7 +4,7 @@ import appReducer, { createAppDefaultState } from './app.reducer';
 describe('app.reducer', () => {
 
 	it('should handle DIALOG_OPEN', async () => {
-		const state: AppState = createAppDefaultState({}, {});
+		const state: AppState = createAppDefaultState({});
 
 		let newState = appReducer(state, {
 			type: 'DIALOG_OPEN',
@@ -49,7 +49,7 @@ describe('app.reducer', () => {
 
 	it('showing a dialog in one window should hide dialogs with the same ID in background windows', () => {
 		const state: AppState = {
-			...createAppDefaultState({}, {}),
+			...createAppDefaultState({}),
 			backgroundWindows: {
 				testWindow: {
 					...createAppDefaultWindowState(),
diff --git a/packages/app-desktop/commands/exportDeletionLog.test.ts b/packages/app-desktop/commands/exportDeletionLog.test.ts
index fcae872f8b2..c7c450de495 100644
--- a/packages/app-desktop/commands/exportDeletionLog.test.ts
+++ b/packages/app-desktop/commands/exportDeletionLog.test.ts
@@ -38,7 +38,7 @@ describe('exportDeletionLog', () => {
 	let state: AppState = undefined;
 
 	beforeAll(() => {
-		state = createAppDefaultState({}, {});
+		state = createAppDefaultState({});
 		jest.useFakeTimers();
 		jest.setSystemTime(new Date('2024-09-18T12:00:00Z').getTime());
 	});
diff --git a/packages/app-desktop/services/sortOrder/PerFolderSortOrderService.test.ts b/packages/app-desktop/services/sortOrder/PerFolderSortOrderService.test.ts
index d0718f06c4e..00bee4d4f43 100644
--- a/packages/app-desktop/services/sortOrder/PerFolderSortOrderService.test.ts
+++ b/packages/app-desktop/services/sortOrder/PerFolderSortOrderService.test.ts
@@ -39,7 +39,7 @@ describe('PerFolderSortOrderService', () => {
 	beforeEach(() => {
 		PerFolderSortOrderService.initialize();
 		Setting.setValue('notes.perFolderSortOrderEnabled', true);
-		updateAppState(createAppDefaultState({}, {}));
+		updateAppState(createAppDefaultState({}));
 		switchToFolder(folderId1);
 	});
 	afterEach(() => {