fix: improve desktop resize responsiveness
This commit is contained in:
@@ -182,6 +182,7 @@ const state = {
|
|||||||
windowCounter: 1,
|
windowCounter: 1,
|
||||||
focusedWindowIds: new Set(),
|
focusedWindowIds: new Set(),
|
||||||
windowGeometryRevisions: new Map(),
|
windowGeometryRevisions: new Map(),
|
||||||
|
windowGeometryTimers: new Map(),
|
||||||
miniChatWindowsBySession: new Map(),
|
miniChatWindowsBySession: new Map(),
|
||||||
sshStatuses: new Map(),
|
sshStatuses: new Map(),
|
||||||
sshLogs: new Map(),
|
sshLogs: new Map(),
|
||||||
@@ -581,8 +582,15 @@ const debounceWindowStatePersist = (browserWindow, immediate = false) => {
|
|||||||
const revision = (state.windowGeometryRevisions.get(key) || 0) + 1;
|
const revision = (state.windowGeometryRevisions.get(key) || 0) + 1;
|
||||||
state.windowGeometryRevisions.set(key, revision);
|
state.windowGeometryRevisions.set(key, revision);
|
||||||
|
|
||||||
|
const existingTimer = state.windowGeometryTimers.get(key);
|
||||||
|
if (existingTimer) {
|
||||||
|
clearTimeout(existingTimer);
|
||||||
|
state.windowGeometryTimers.delete(key);
|
||||||
|
}
|
||||||
|
|
||||||
const persist = async () => {
|
const persist = async () => {
|
||||||
if (state.windowGeometryRevisions.get(key) !== revision) return;
|
if (state.windowGeometryRevisions.get(key) !== revision) return;
|
||||||
|
state.windowGeometryTimers.delete(key);
|
||||||
await writeWindowState(browserWindow);
|
await writeWindowState(browserWindow);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -591,9 +599,10 @@ const debounceWindowStatePersist = (browserWindow, immediate = false) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
void persist();
|
void persist();
|
||||||
}, 300);
|
}, 300);
|
||||||
|
state.windowGeometryTimers.set(key, timer);
|
||||||
};
|
};
|
||||||
|
|
||||||
const buildHealthUrl = (url) => {
|
const buildHealthUrl = (url) => {
|
||||||
@@ -1848,7 +1857,9 @@ const createBrowserWindow = ({ label, restoreGeometry, url, runtimeConfig = {} }
|
|||||||
}
|
}
|
||||||
|
|
||||||
browserWindow.on('resize', () => {
|
browserWindow.on('resize', () => {
|
||||||
emitToWindow(browserWindow, 'openchamber:window-resized');
|
if (process.platform === 'darwin') {
|
||||||
|
emitToWindow(browserWindow, 'openchamber:window-resized');
|
||||||
|
}
|
||||||
debounceWindowStatePersist(browserWindow, false);
|
debounceWindowStatePersist(browserWindow, false);
|
||||||
});
|
});
|
||||||
browserWindow.on('maximize', () => {
|
browserWindow.on('maximize', () => {
|
||||||
|
|||||||
@@ -271,24 +271,24 @@ export const MainLayout: React.FC = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let timeoutId: number | undefined;
|
let frameId: number | undefined;
|
||||||
|
|
||||||
const handleResize = () => {
|
const handleResize = () => {
|
||||||
if (timeoutId !== undefined) {
|
if (frameId !== undefined) {
|
||||||
window.clearTimeout(timeoutId);
|
return;
|
||||||
}
|
}
|
||||||
|
frameId = window.requestAnimationFrame(() => {
|
||||||
timeoutId = window.setTimeout(() => {
|
frameId = undefined;
|
||||||
useUIStore.getState().updateProportionalSidebarWidths();
|
useUIStore.getState().updateProportionalSidebarWidths();
|
||||||
}, 150);
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
window.addEventListener('resize', handleResize);
|
window.addEventListener('resize', handleResize);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener('resize', handleResize);
|
window.removeEventListener('resize', handleResize);
|
||||||
if (timeoutId !== undefined) {
|
if (frameId !== undefined) {
|
||||||
window.clearTimeout(timeoutId);
|
window.cancelAnimationFrame(frameId);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
@@ -298,7 +298,7 @@ export const MainLayout: React.FC = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let timeoutId: number | undefined;
|
let frameId: number | undefined;
|
||||||
|
|
||||||
const handleResponsivePanels = () => {
|
const handleResponsivePanels = () => {
|
||||||
const state = useUIStore.getState();
|
const state = useUIStore.getState();
|
||||||
@@ -340,13 +340,13 @@ export const MainLayout: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleResize = () => {
|
const handleResize = () => {
|
||||||
if (timeoutId !== undefined) {
|
if (frameId !== undefined) {
|
||||||
window.clearTimeout(timeoutId);
|
return;
|
||||||
}
|
}
|
||||||
|
frameId = window.requestAnimationFrame(() => {
|
||||||
timeoutId = window.setTimeout(() => {
|
frameId = undefined;
|
||||||
handleResponsivePanels();
|
handleResponsivePanels();
|
||||||
}, 100);
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
handleResponsivePanels();
|
handleResponsivePanels();
|
||||||
@@ -354,8 +354,8 @@ export const MainLayout: React.FC = () => {
|
|||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener('resize', handleResize);
|
window.removeEventListener('resize', handleResize);
|
||||||
if (timeoutId !== undefined) {
|
if (frameId !== undefined) {
|
||||||
window.clearTimeout(timeoutId);
|
window.cancelAnimationFrame(frameId);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [isMobile, isTablet, setBottomTerminalOpen, setRightSidebarOpen]);
|
}, [isMobile, isTablet, setBottomTerminalOpen, setRightSidebarOpen]);
|
||||||
|
|||||||
+135
-83
@@ -29,6 +29,17 @@ export const BREAKPOINTS = {
|
|||||||
'2xl': 1536,
|
'2xl': 1536,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
const DEFAULT_DEVICE_INFO: DeviceInfo = {
|
||||||
|
isMobile: false,
|
||||||
|
isTablet: false,
|
||||||
|
isDesktop: true,
|
||||||
|
deviceType: 'desktop',
|
||||||
|
screenWidth: 1024,
|
||||||
|
breakpoint: 'lg',
|
||||||
|
hasTouchInput: false,
|
||||||
|
hasTouchOnlyPointer: false,
|
||||||
|
};
|
||||||
|
|
||||||
const getNavigatorDeviceHints = (maxTouchPoints: number) => {
|
const getNavigatorDeviceHints = (maxTouchPoints: number) => {
|
||||||
if (typeof navigator === 'undefined') {
|
if (typeof navigator === 'undefined') {
|
||||||
return { isExplicitTablet: false };
|
return { isExplicitTablet: false };
|
||||||
@@ -146,6 +157,125 @@ export function getDeviceInfo(): DeviceInfo {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isSameDeviceInfo = (left: DeviceInfo, right: DeviceInfo): boolean => (
|
||||||
|
left.isMobile === right.isMobile
|
||||||
|
&& left.isTablet === right.isTablet
|
||||||
|
&& left.isDesktop === right.isDesktop
|
||||||
|
&& left.deviceType === right.deviceType
|
||||||
|
&& left.screenWidth === right.screenWidth
|
||||||
|
&& left.breakpoint === right.breakpoint
|
||||||
|
&& left.hasTouchInput === right.hasTouchInput
|
||||||
|
&& left.hasTouchOnlyPointer === right.hasTouchOnlyPointer
|
||||||
|
);
|
||||||
|
|
||||||
|
const deviceInfoSubscribers = new Set<() => void>();
|
||||||
|
let deviceInfoSnapshot: DeviceInfo | null = null;
|
||||||
|
let deviceInfoFrameId: number | undefined;
|
||||||
|
let cleanupDeviceInfoSource: (() => void) | null = null;
|
||||||
|
|
||||||
|
const readDeviceInfoSnapshot = (): DeviceInfo => {
|
||||||
|
if (typeof window === 'undefined') {
|
||||||
|
return DEFAULT_DEVICE_INFO;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!deviceInfoSnapshot) {
|
||||||
|
deviceInfoSnapshot = getDeviceInfo();
|
||||||
|
}
|
||||||
|
|
||||||
|
return deviceInfoSnapshot;
|
||||||
|
};
|
||||||
|
|
||||||
|
const notifyDeviceInfoSubscribers = () => {
|
||||||
|
for (const listener of deviceInfoSubscribers) {
|
||||||
|
listener();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateDeviceInfoSnapshot = () => {
|
||||||
|
deviceInfoFrameId = undefined;
|
||||||
|
const next = getDeviceInfo();
|
||||||
|
if (deviceInfoSnapshot && isSameDeviceInfo(deviceInfoSnapshot, next)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
deviceInfoSnapshot = next;
|
||||||
|
notifyDeviceInfoSubscribers();
|
||||||
|
};
|
||||||
|
|
||||||
|
const scheduleDeviceInfoUpdate = () => {
|
||||||
|
if (typeof window === 'undefined' || deviceInfoFrameId !== undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
deviceInfoFrameId = window.requestAnimationFrame(updateDeviceInfoSnapshot);
|
||||||
|
};
|
||||||
|
|
||||||
|
const attachMediaQueryListener = (query: MediaQueryList | null, listener: () => void): (() => void) => {
|
||||||
|
if (!query) {
|
||||||
|
return () => {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof query.addEventListener === 'function') {
|
||||||
|
query.addEventListener('change', listener);
|
||||||
|
return () => query.removeEventListener('change', listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof query.addListener === 'function') {
|
||||||
|
query.addListener(listener);
|
||||||
|
return () => query.removeListener(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {};
|
||||||
|
};
|
||||||
|
|
||||||
|
const startDeviceInfoSource = (): (() => void) => {
|
||||||
|
if (typeof window === 'undefined') {
|
||||||
|
return () => {};
|
||||||
|
}
|
||||||
|
|
||||||
|
deviceInfoSnapshot = getDeviceInfo();
|
||||||
|
window.addEventListener('resize', scheduleDeviceInfoUpdate);
|
||||||
|
|
||||||
|
const pointerQuery = typeof window.matchMedia === 'function'
|
||||||
|
? window.matchMedia('(pointer: coarse)')
|
||||||
|
: null;
|
||||||
|
const hoverQuery = typeof window.matchMedia === 'function'
|
||||||
|
? window.matchMedia('(hover: none)')
|
||||||
|
: null;
|
||||||
|
const cleanupPointer = attachMediaQueryListener(pointerQuery, scheduleDeviceInfoUpdate);
|
||||||
|
const cleanupHover = attachMediaQueryListener(hoverQuery, scheduleDeviceInfoUpdate);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('resize', scheduleDeviceInfoUpdate);
|
||||||
|
cleanupPointer();
|
||||||
|
cleanupHover();
|
||||||
|
if (deviceInfoFrameId !== undefined) {
|
||||||
|
window.cancelAnimationFrame(deviceInfoFrameId);
|
||||||
|
deviceInfoFrameId = undefined;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const subscribeDeviceInfo = (listener: () => void): (() => void) => {
|
||||||
|
if (typeof window === 'undefined') {
|
||||||
|
return () => {};
|
||||||
|
}
|
||||||
|
|
||||||
|
deviceInfoSubscribers.add(listener);
|
||||||
|
if (!cleanupDeviceInfoSource) {
|
||||||
|
cleanupDeviceInfoSource = startDeviceInfoSource();
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
deviceInfoSubscribers.delete(listener);
|
||||||
|
if (deviceInfoSubscribers.size === 0 && cleanupDeviceInfoSource) {
|
||||||
|
cleanupDeviceInfoSource();
|
||||||
|
cleanupDeviceInfoSource = null;
|
||||||
|
deviceInfoSnapshot = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export function isMobileDeviceViaCSS(): boolean {
|
export function isMobileDeviceViaCSS(): boolean {
|
||||||
if (typeof window === 'undefined') return false;
|
if (typeof window === 'undefined') return false;
|
||||||
|
|
||||||
@@ -224,87 +354,9 @@ export function useTabletStandalonePwaRuntime(): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function useDeviceInfo(): DeviceInfo {
|
export function useDeviceInfo(): DeviceInfo {
|
||||||
const [deviceInfo, setDeviceInfo] = React.useState<DeviceInfo>(() => {
|
return React.useSyncExternalStore(
|
||||||
if (typeof window === 'undefined') {
|
subscribeDeviceInfo,
|
||||||
return {
|
readDeviceInfoSnapshot,
|
||||||
isMobile: false,
|
() => DEFAULT_DEVICE_INFO,
|
||||||
isTablet: false,
|
);
|
||||||
isDesktop: true,
|
|
||||||
deviceType: 'desktop',
|
|
||||||
screenWidth: 1024,
|
|
||||||
breakpoint: 'lg',
|
|
||||||
hasTouchInput: false,
|
|
||||||
hasTouchOnlyPointer: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return getDeviceInfo();
|
|
||||||
});
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (typeof window === 'undefined') return;
|
|
||||||
|
|
||||||
let debounceTimer: ReturnType<typeof setTimeout>;
|
|
||||||
const handleResize = () => {
|
|
||||||
clearTimeout(debounceTimer);
|
|
||||||
debounceTimer = setTimeout(() => {
|
|
||||||
setDeviceInfo(getDeviceInfo());
|
|
||||||
}, 150);
|
|
||||||
};
|
|
||||||
|
|
||||||
window.addEventListener('resize', handleResize);
|
|
||||||
return () => {
|
|
||||||
clearTimeout(debounceTimer);
|
|
||||||
window.removeEventListener('resize', handleResize);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const pointerQuery = window.matchMedia('(pointer: coarse)');
|
|
||||||
const hoverQuery = window.matchMedia('(hover: none)');
|
|
||||||
|
|
||||||
const handlePointerChange = () => {
|
|
||||||
setDeviceInfo(getDeviceInfo());
|
|
||||||
};
|
|
||||||
|
|
||||||
const cleanups: Array<() => void> = [];
|
|
||||||
|
|
||||||
const attachListener = (query: MediaQueryList | null) => {
|
|
||||||
if (!query) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (typeof query.addEventListener === 'function') {
|
|
||||||
query.addEventListener('change', handlePointerChange);
|
|
||||||
cleanups.push(() => query.removeEventListener('change', handlePointerChange));
|
|
||||||
} else if (typeof query.addListener === 'function') {
|
|
||||||
query.addListener(handlePointerChange);
|
|
||||||
cleanups.push(() => query.removeListener(handlePointerChange));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
attachListener(pointerQuery);
|
|
||||||
attachListener(hoverQuery);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
cleanups.forEach((cleanup) => cleanup());
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (typeof window === 'undefined') return;
|
|
||||||
const isDesktopShellRuntime = isDesktopShell();
|
|
||||||
const supportsMatchMedia = typeof window.matchMedia === 'function';
|
|
||||||
const pointerQuery = supportsMatchMedia ? window.matchMedia('(pointer: coarse)') : null;
|
|
||||||
const hoverQuery = supportsMatchMedia ? window.matchMedia('(hover: none)') : null;
|
|
||||||
const prefersCoarsePointer = pointerQuery?.matches ?? false;
|
|
||||||
const noHover = hoverQuery?.matches ?? false;
|
|
||||||
const maxTouchPoints = typeof navigator !== 'undefined' ? navigator.maxTouchPoints ?? 0 : 0;
|
|
||||||
const hasTouchInput = prefersCoarsePointer || noHover || maxTouchPoints > 0;
|
|
||||||
setRootDeviceAttributes(isDesktopShellRuntime, deviceInfo.deviceType, hasTouchInput);
|
|
||||||
}, [deviceInfo.deviceType, deviceInfo.hasTouchInput]);
|
|
||||||
|
|
||||||
return deviceInfo;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1854,10 +1854,13 @@ export const useUIStore = create<UIStore>()(
|
|||||||
const updates: Partial<UIStore> = {};
|
const updates: Partial<UIStore> = {};
|
||||||
|
|
||||||
if (state.isBottomTerminalOpen && !state.hasManuallyResizedBottomTerminal) {
|
if (state.isBottomTerminalOpen && !state.hasManuallyResizedBottomTerminal) {
|
||||||
updates.bottomTerminalHeight = Math.floor(window.innerHeight * 0.32);
|
const nextHeight = Math.floor(window.innerHeight * 0.32);
|
||||||
|
if (state.bottomTerminalHeight !== nextHeight) {
|
||||||
|
updates.bottomTerminalHeight = nextHeight;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return updates;
|
return Object.keys(updates).length > 0 ? updates : state;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user