From d229da2bb90b07e4483c8fa73d801a626c573ce7 Mon Sep 17 00:00:00 2001 From: Antonio Date: Mon, 12 Oct 2026 18:05:01 +0200 Subject: [PATCH 1/3] feat(screen-share): let users choose which screen or window to share --- public/locales/en/translations.json | 7 + public/locales/it/translations.json | 7 + .../ScreenSharePickerController.ts | 198 ++++++++++++++ .../windows/ScreenSharePickerWindow.ts | 45 ++++ src/main/classes/windows/index.ts | 1 + src/main/lib/ipcEvents.ts | 15 -- src/main/main.ts | 5 + .../public/locales/en/translations.json | 7 + .../public/locales/it/translations.json | 7 + src/renderer/src/App.tsx | 5 + .../src/hooks/usePhoneIslandEventListeners.ts | 3 - src/renderer/src/pages/PhoneIslandPage.tsx | 24 -- .../src/pages/ScreenSharePickerPage.tsx | 241 ++++++++++++++++++ src/renderer/src/pages/index.ts | 1 + src/shared/constants.ts | 4 +- src/shared/types.ts | 9 + 16 files changed, 535 insertions(+), 44 deletions(-) create mode 100644 src/main/classes/controllers/ScreenSharePickerController.ts create mode 100644 src/main/classes/windows/ScreenSharePickerWindow.ts create mode 100644 src/renderer/src/pages/ScreenSharePickerPage.tsx diff --git a/public/locales/en/translations.json b/public/locales/en/translations.json index d8b17718..711601fd 100644 --- a/public/locales/en/translations.json +++ b/public/locales/en/translations.json @@ -994,6 +994,13 @@ "Placeholder": "Search contact or dial number...", "Call": "Call" }, + "ScreenShare": { + "Title": "Choose what to share", + "Entire screen": "Entire screen", + "Screen": "Screen {{number}}", + "Window": "Window", + "Share": "Share" + }, "Errors": { "browser_permissions": "Browser permissions error", "user_permissions": "Media permissions error", diff --git a/public/locales/it/translations.json b/public/locales/it/translations.json index 7e5bf0d9..c021fc24 100644 --- a/public/locales/it/translations.json +++ b/public/locales/it/translations.json @@ -994,6 +994,13 @@ "Placeholder": "Cerca contatto o componi numero...", "Call": "Chiama" }, + "ScreenShare": { + "Title": "Scegli cosa condividere", + "Entire screen": "Schermo intero", + "Screen": "Schermo {{number}}", + "Window": "Finestra", + "Share": "Condividi" + }, "Errors": { "browser_permissions": "Errore nei permessi del browser", "user_permissions": "Errore nei permessi dei media", diff --git a/src/main/classes/controllers/ScreenSharePickerController.ts b/src/main/classes/controllers/ScreenSharePickerController.ts new file mode 100644 index 00000000..8d4f21df --- /dev/null +++ b/src/main/classes/controllers/ScreenSharePickerController.ts @@ -0,0 +1,198 @@ +import { + BrowserWindow, + DesktopCapturerSource, + desktopCapturer, + screen, + session, +} from 'electron' +import { IPC_EVENTS } from '@shared/constants' +import { ScreenShareSource } from '@shared/types' +import { Log } from '@shared/utils/logger' +import { SCREEN_SHARE_PICKER_SIZE, ScreenSharePickerWindow } from '../windows' +import { PhoneIslandController } from './PhoneIslandController' + +type SettleRequest = (source?: DesktopCapturerSource) => void + +const PICKER_LOAD_TIMEOUT_MS = 10000 + +export class ScreenSharePickerController { + static instance: ScreenSharePickerController + private window: ScreenSharePickerWindow | undefined + private settlePending: SettleRequest | undefined + + constructor() { + ScreenSharePickerController.instance = this + session.defaultSession.setDisplayMediaRequestHandler((_request, callback) => + this.handleRequest(callback), + ) + } + + cancel() { + this.settlePending?.() + } + + private async handleRequest(callback: (streams: Electron.Streams) => void) { + if (this.settlePending) { + Log.warning('SCREEN SHARE request rejected: picker already open') + callback(null as unknown as Electron.Streams) + return + } + + let settled = false + const restorePhoneIsland = this.lowerPhoneIslandForSystemPicker() + const settle: SettleRequest = (source) => { + if (settled) return + settled = true + this.settlePending = undefined + this.destroyWindow() + restorePhoneIsland() + if (source) { + Log.info('SCREEN SHARE source selected', source.id, source.name) + callback({ video: { id: source.id, name: source.name } }) + } else { + Log.info('SCREEN SHARE request cancelled') + callback(null as unknown as Electron.Streams) + } + } + this.settlePending = settle + + try { + const sources = await this.getSources() + if (settled) return + if (sources.length <= 1) { + settle(sources[0]) + return + } + this.openPicker(sources, settle) + } catch (e) { + Log.error('SCREEN SHARE getSources error', e) + settle() + } + } + + private lowerPhoneIslandForSystemPicker(): () => void { + const isWayland = + process.platform === 'linux' && + (process.env['XDG_SESSION_TYPE'] === 'wayland' || + !!process.env['WAYLAND_DISPLAY']) + try { + const window = PhoneIslandController.instance?.window?.getWindow() + if ( + !isWayland || + !window || + window.isDestroyed() || + !window.isAlwaysOnTop() + ) { + return () => {} + } + window.setAlwaysOnTop(false) + return () => { + try { + if (!window.isDestroyed()) { + window.setAlwaysOnTop(true, 'screen-saver') + } + } catch (e) { + Log.warning('SCREEN SHARE phone island restore error', e) + } + } + } catch (e) { + Log.warning('SCREEN SHARE phone island lowering error', e) + return () => {} + } + } + + private async getSources(): Promise { + const ownMediaSourceIds = new Set( + BrowserWindow.getAllWindows() + .filter((w) => !w.isDestroyed()) + .map((w) => w.getMediaSourceId()), + ) + const sources = await desktopCapturer.getSources({ + types: ['screen', 'window'], + thumbnailSize: { width: 320, height: 180 }, + fetchWindowIcons: true, + }) + return sources.filter((s) => !ownMediaSourceIds.has(s.id)) + } + + private openPicker(sources: DesktopCapturerSource[], settle: SettleRequest) { + const pickerWindow = new ScreenSharePickerWindow() + this.window = pickerWindow + const window = pickerWindow.getWindow() + if (!window) { + settle() + return + } + + const serializedSources: ScreenShareSource[] = sources.map((s) => ({ + id: s.id, + name: s.name, + type: s.id.startsWith('screen:') ? 'screen' : 'window', + thumbnail: s.thumbnail.toDataURL(), + appIcon: + s.appIcon && !s.appIcon.isEmpty() ? s.appIcon.toDataURL() : undefined, + })) + + const loadTimeout = setTimeout(() => { + Log.warning('SCREEN SHARE picker did not load') + settle() + }, PICKER_LOAD_TIMEOUT_MS) + + window.on('closed', () => { + clearTimeout(loadTimeout) + settle() + }) + window.webContents.on('render-process-gone', () => settle()) + window.webContents.on('did-fail-load', () => settle()) + + pickerWindow.addListener(IPC_EVENTS.SCREEN_SHARE_PICKER_SOURCES, () => { + clearTimeout(loadTimeout) + pickerWindow.emit( + IPC_EVENTS.SCREEN_SHARE_PICKER_SOURCES, + serializedSources, + ) + this.showCentered(window) + }) + + pickerWindow.addListener( + IPC_EVENTS.SCREEN_SHARE_PICKER_SELECT, + (_event, sourceId: string | null) => { + settle(sources.find((s) => s.id === sourceId)) + }, + ) + } + + private showCentered(window: BrowserWindow) { + try { + const phoneIslandBounds = PhoneIslandController.instance?.window + ?.getWindow() + ?.getBounds() + const display = + phoneIslandBounds && + phoneIslandBounds.width > 1 && + phoneIslandBounds.height > 1 + ? screen.getDisplayMatching(phoneIslandBounds) + : screen.getDisplayNearestPoint(screen.getCursorScreenPoint()) + const { x, y, width, height } = display.workArea + const { width: w, height: h } = SCREEN_SHARE_PICKER_SIZE + window.setBounds({ + x: x + Math.round((width - w) / 2), + y: y + Math.round((height - h) / 2), + width: w, + height: h, + }) + } catch (e) { + Log.warning('SCREEN SHARE picker positioning error', e) + } + window.show() + window.focus() + } + + private destroyWindow() { + const window = this.window?.getWindow() + this.window = undefined + if (window && !window.isDestroyed()) { + window.destroy() + } + } +} diff --git a/src/main/classes/windows/ScreenSharePickerWindow.ts b/src/main/classes/windows/ScreenSharePickerWindow.ts new file mode 100644 index 00000000..98509690 --- /dev/null +++ b/src/main/classes/windows/ScreenSharePickerWindow.ts @@ -0,0 +1,45 @@ +import { PAGES } from '@shared/types' +import { BaseWindow } from './BaseWindow' + +export const SCREEN_SHARE_PICKER_SIZE = { width: 720, height: 520 } + +export class ScreenSharePickerWindow extends BaseWindow { + constructor() { + super(PAGES.SCREENSHAREPICKER, { + width: SCREEN_SHARE_PICKER_SIZE.width, + height: SCREEN_SHARE_PICKER_SIZE.height, + show: false, + focusable: true, + fullscreenable: false, + autoHideMenuBar: true, + closable: true, + alwaysOnTop: true, + minimizable: false, + maximizable: false, + movable: true, + resizable: false, + skipTaskbar: true, + roundedCorners: true, + parent: undefined, + transparent: true, + hiddenInMissionControl: true, + hasShadow: false, + center: true, + fullscreen: false, + enableLargerThanScreen: false, + frame: false, + thickFrame: false, + trafficLightPosition: { x: 0, y: 0 }, + webPreferences: { + nodeIntegration: true, + }, + }) + + this.addOnBuildListener(() => { + const window = this.getWindow() + if (window) { + window.setAlwaysOnTop(true, 'screen-saver') + } + }) + } +} diff --git a/src/main/classes/windows/index.ts b/src/main/classes/windows/index.ts index 7cc92b28..69be4641 100644 --- a/src/main/classes/windows/index.ts +++ b/src/main/classes/windows/index.ts @@ -4,3 +4,4 @@ export * from './NethLinkWindow' export * from './PhoneIslandWindow' export * from './DevToolsWindow' export * from './CommandBarWindow' +export * from './ScreenSharePickerWindow' diff --git a/src/main/lib/ipcEvents.ts b/src/main/lib/ipcEvents.ts index ac720809..54a752a1 100644 --- a/src/main/lib/ipcEvents.ts +++ b/src/main/lib/ipcEvents.ts @@ -11,7 +11,6 @@ import { net, screen, shell, - desktopCapturer, globalShortcut, clipboard, Notification, @@ -732,20 +731,6 @@ export function registerIpcEvents() { Log.error('EXIT FULLSCREEN error ', e) } }) - ipcMain.on(IPC_EVENTS.SCREEN_SHARE_INIT, () => { - desktopCapturer - .getSources({ types: ['screen'] }) // allow only entire screen sharing - .then((sources) => { - PhoneIslandController.instance?.window?.emit( - IPC_EVENTS.SCREEN_SHARE_SOURCES, - sources, - ) - }) - .catch((e) => { - Log.error('ENTER SCREEN_SHARE_INIT error ', e) - }) - }) - ipcMain.on(IPC_EVENTS.URL_OPEN, (_, data) => { try { PhoneIslandController.instance.window.emit(IPC_EVENTS.URL_OPEN, data) diff --git a/src/main/main.ts b/src/main/main.ts index 602595ec..aa72c804 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -21,6 +21,7 @@ import { import { AccountController } from './classes/controllers' import { PhoneIslandController } from './classes/controllers/PhoneIslandController' import { CommandBarController } from './classes/controllers/CommandBarController' +import { ScreenSharePickerController } from './classes/controllers/ScreenSharePickerController' import { Account, AuthAppData, @@ -130,6 +131,7 @@ function startup() { Log.warning('Failed to disable Command Bar on logout:', e) } + ScreenSharePickerController.instance?.cancel() await PhoneIslandController.instance.logout() NethLinkController.instance.logout() AccountController.instance.logout() @@ -274,6 +276,7 @@ function attachOnReadyProcess() { // I create the Tray controller instance - I define to it the function it should execute upon clicking on the icon new SplashScreenController() new TrayController() + new ScreenSharePickerController() // I display the splashscreen when the splashscreen component is correctly loaded. SplashScreenController.instance.window.addOnBuildListener(() => { @@ -546,6 +549,8 @@ function attachOnReadyProcess() { await CommandBarController.instance.safeQuit() } + ScreenSharePickerController.instance?.cancel() + Log.info('APP QUIT CORRECTLY') app.exit() }) diff --git a/src/renderer/public/locales/en/translations.json b/src/renderer/public/locales/en/translations.json index 16883fba..9331cbb4 100644 --- a/src/renderer/public/locales/en/translations.json +++ b/src/renderer/public/locales/en/translations.json @@ -994,6 +994,13 @@ "Placeholder": "Search contact or dial number...", "Call": "Call" }, + "ScreenShare": { + "Title": "Choose what to share", + "Entire screen": "Entire screen", + "Screen": "Screen {{number}}", + "Window": "Window", + "Share": "Share" + }, "Errors": { "browser_permissions": "Browser permissions error", "user_permissions": "Media permissions error", diff --git a/src/renderer/public/locales/it/translations.json b/src/renderer/public/locales/it/translations.json index 468aa8de..717893da 100644 --- a/src/renderer/public/locales/it/translations.json +++ b/src/renderer/public/locales/it/translations.json @@ -994,6 +994,13 @@ "Placeholder": "Cerca contatto o componi numero...", "Call": "Chiama" }, + "ScreenShare": { + "Title": "Scegli cosa condividere", + "Entire screen": "Schermo intero", + "Screen": "Schermo {{number}}", + "Window": "Finestra", + "Share": "Condividi" + }, "Errors": { "browser_permissions": "Errore nei permessi del browser", "user_permissions": "Errore nei permessi dei media", diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index 5140e346..f14b69a7 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -6,6 +6,7 @@ import { SplashScreenPage, NethLinkPage, CommandBarPage, + ScreenSharePickerPage, } from '@/pages' import { loadI18n } from './lib/i18n' import { Log } from '@shared/utils/logger' @@ -143,6 +144,10 @@ const RequestStateComponent = () => { path: PAGES.COMMANDBAR, element: , }, + { + path: PAGES.SCREENSHAREPICKER, + element: , + }, ], }, ]) diff --git a/src/renderer/src/hooks/usePhoneIslandEventListeners.ts b/src/renderer/src/hooks/usePhoneIslandEventListeners.ts index c2daafa2..7e688cdf 100644 --- a/src/renderer/src/hooks/usePhoneIslandEventListeners.ts +++ b/src/renderer/src/hooks/usePhoneIslandEventListeners.ts @@ -453,9 +453,6 @@ export const usePhoneIslandEventListener = () => { ), ...eventHandler( PHONE_ISLAND_EVENTS['phone-island-screen-share-initialized'], - () => { - window.electron.send(IPC_EVENTS.SCREEN_SHARE_INIT) - }, ), ...eventHandler( PHONE_ISLAND_EVENTS['phone-island-url-parameter-opened-external'], diff --git a/src/renderer/src/pages/PhoneIslandPage.tsx b/src/renderer/src/pages/PhoneIslandPage.tsx index 01dd4ccb..923c3aac 100644 --- a/src/renderer/src/pages/PhoneIslandPage.tsx +++ b/src/renderer/src/pages/PhoneIslandPage.tsx @@ -46,30 +46,6 @@ export function PhoneIslandPage() { window.electron.receive(IPC_EVENTS.LOGOUT, logout) - window.electron.receive(IPC_EVENTS.SCREEN_SHARE_SOURCES, (sources: any) => { - if (typeof navigator.mediaDevices.getDisplayMedia === 'function') { - navigator.mediaDevices.getDisplayMedia = async (constraints) => { - // choose always Entire screen to share, add dialog in the future - // to choose single applications or windows - const selectedSource = sources.find( - (source) => - source.id.startsWith('screen:') || - source.name.toLowerCase().includes('screen') || - source[0], - ) - const stream = await navigator.mediaDevices.getUserMedia({ - video: { - mandatory: { - chromeMediaSource: 'desktop', - chromeMediaSourceId: selectedSource.id, - }, - } as any, - }) - return stream - } - } - }) - window.electron.receive(IPC_EVENTS.START_CALL, (number: string) => { eventDispatch(PHONE_ISLAND_EVENTS['phone-island-call-start'], { number, diff --git a/src/renderer/src/pages/ScreenSharePickerPage.tsx b/src/renderer/src/pages/ScreenSharePickerPage.tsx new file mode 100644 index 00000000..ef006823 --- /dev/null +++ b/src/renderer/src/pages/ScreenSharePickerPage.tsx @@ -0,0 +1,241 @@ +import { + CSSProperties, + useCallback, + useEffect, + useId, + useMemo, + useState, +} from 'react' +import { t } from 'i18next' +import classNames from 'classnames' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { + IconDefinition, + faDesktop, + faWindowMaximize, +} from '@fortawesome/free-solid-svg-icons' +import { IPC_EVENTS } from '@shared/constants' +import { ScreenShareSource } from '@shared/types' +import { useSharedState } from '@renderer/store' +import { parseThemeToClassName } from '@renderer/utils' +import { Button } from '@renderer/components/Nethesis' +import { CustomThemedTooltip } from '@renderer/components/Nethesis/CustomThemedTooltip' +import { useIsTruncated } from '@renderer/hooks/useIsTruncated' + +type SourceTab = 'screen' | 'window' + +const SourceCard = ({ + source, + label, + isSelected, + onSelect, + onConfirm, +}: { + source: ScreenShareSource + label: string + isSelected: boolean + onSelect: () => void + onConfirm: () => void +}) => { + const tooltipId = useId() + const [labelRef, isLabelTruncated] = useIsTruncated([label]) + + return ( + + ) +} + +export function ScreenSharePickerPage() { + const [theme] = useSharedState('theme') + const [sources, setSources] = useState([]) + const [activeTab, setActiveTab] = useState('screen') + const [selectedId, setSelectedId] = useState(null) + + const screens = useMemo( + () => sources.filter((s) => s.type === 'screen'), + [sources], + ) + const windows = useMemo( + () => sources.filter((s) => s.type === 'window'), + [sources], + ) + const visibleSources = activeTab === 'screen' ? screens : windows + + useEffect(() => { + window.electron.receive( + IPC_EVENTS.SCREEN_SHARE_PICKER_SOURCES, + (receivedSources: ScreenShareSource[]) => { + setSources(receivedSources) + setActiveTab( + receivedSources.some((s) => s.type === 'screen') + ? 'screen' + : 'window', + ) + }, + ) + window.electron.send(IPC_EVENTS.SCREEN_SHARE_PICKER_SOURCES) + return () => { + window.electron.removeAllListeners(IPC_EVENTS.SCREEN_SHARE_PICKER_SOURCES) + } + }, []) + + const select = useCallback((sourceId: string | null) => { + window.electron.send(IPC_EVENTS.SCREEN_SHARE_PICKER_SELECT, sourceId) + }, []) + + const onCancel = useCallback(() => select(null), [select]) + + const onShare = useCallback(() => { + if (selectedId) select(selectedId) + }, [select, selectedId]) + + useEffect(() => { + const onKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Escape') { + onCancel() + } else if (e.key === 'Enter') { + const target = e.target as HTMLElement + const card = target.closest('[data-source-card]') + if ( + card + ? card.getAttribute('data-source-card') !== selectedId + : target.closest('button') + ) { + return + } + e.preventDefault() + onShare() + } + } + window.addEventListener('keydown', onKeyDown) + return () => window.removeEventListener('keydown', onKeyDown) + }, [onCancel, onShare, selectedId]) + + const changeTab = (tab: SourceTab) => { + setActiveTab(tab) + setSelectedId(null) + } + + const getLabel = (source: ScreenShareSource, index: number) => { + if (source.type === 'window') return source.name + return screens.length > 1 + ? t('ScreenShare.Screen', { number: index + 1 }) + : t('ScreenShare.Entire screen') + } + + const tabs: { id: SourceTab; label: string; icon: IconDefinition }[] = [ + { id: 'screen', label: t('ScreenShare.Entire screen'), icon: faDesktop }, + { id: 'window', label: t('ScreenShare.Window'), icon: faWindowMaximize }, + ] + + return ( +
+
+
+
+

+ {t('ScreenShare.Title')} +

+
+ + {screens.length > 0 && windows.length > 0 && ( +
+ {tabs.map((tab) => ( + + ))} +
+ )} + +
+
+ {visibleSources.map((source, index) => ( + setSelectedId(source.id)} + onConfirm={() => select(source.id)} + /> + ))} +
+
+ +
+ + +
+
+
+
+ ) +} diff --git a/src/renderer/src/pages/index.ts b/src/renderer/src/pages/index.ts index 1a8c2f1b..1b8922ab 100644 --- a/src/renderer/src/pages/index.ts +++ b/src/renderer/src/pages/index.ts @@ -3,3 +3,4 @@ export * from './NethLinkPage' export * from './PhoneIslandPage' export * from './SplashScreenPage' export * from './CommandBarPage' +export * from './ScreenSharePickerPage' diff --git a/src/shared/constants.ts b/src/shared/constants.ts index d864c871..d0b2d86d 100644 --- a/src/shared/constants.ts +++ b/src/shared/constants.ts @@ -87,8 +87,8 @@ export enum IPC_EVENTS { RESUME = 'RESUME', FULLSCREEN_ENTER = 'FULLSCREEN_ENTER', FULLSCREEN_EXIT = 'FULLSCREEN_EXIT', - SCREEN_SHARE_INIT = 'SCREEN_SHARE_INIT', - SCREEN_SHARE_SOURCES = 'SCREEN_SHARE_SOURCES', + SCREEN_SHARE_PICKER_SOURCES = 'SCREEN_SHARE_PICKER_SOURCES', + SCREEN_SHARE_PICKER_SELECT = 'SCREEN_SHARE_PICKER_SELECT', CHANGE_SHORTCUT = 'CHANGE_SHORTCUT', COPY_TO_CLIPBOARD = 'COPY_TO_CLIPBOARD', CHANGE_PREFERRED_DEVICES = 'CHANGE_PREFERRED_DEVICES', diff --git a/src/shared/types.ts b/src/shared/types.ts index 50298a61..55dc819f 100644 --- a/src/shared/types.ts +++ b/src/shared/types.ts @@ -21,6 +21,7 @@ export enum PAGES { NETHLINK = 'NethLink', DEVTOOLS = 'devtoolspage', COMMANDBAR = 'commandbarpage', + SCREENSHAREPICKER = 'screensharepickerpage', } export type StateType = [T | undefined, (value: T | undefined) => void] @@ -430,6 +431,14 @@ export type PageType = { export type Size = { w: number; h: number } +export type ScreenShareSource = { + id: string + name: string + type: 'screen' | 'window' + thumbnail: string + appIcon?: string +} + export type Device = { type: AvailableDevices id: string From 4efbe5664a169ec6a2ffae7ff8f552e8adcd8872 Mon Sep 17 00:00:00 2001 From: Antonio Date: Thu, 15 Oct 2026 09:53:29 +0200 Subject: [PATCH 2/3] fix(screen-share): picker focus, macOS fullscreen and load timeout --- .../ScreenSharePickerController.ts | 29 +++++++++++++++---- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/src/main/classes/controllers/ScreenSharePickerController.ts b/src/main/classes/controllers/ScreenSharePickerController.ts index 8d4f21df..c36da24e 100644 --- a/src/main/classes/controllers/ScreenSharePickerController.ts +++ b/src/main/classes/controllers/ScreenSharePickerController.ts @@ -8,6 +8,7 @@ import { import { IPC_EVENTS } from '@shared/constants' import { ScreenShareSource } from '@shared/types' import { Log } from '@shared/utils/logger' +import { forceWindowFocus } from '@/lib/windowsFocus' import { SCREEN_SHARE_PICKER_SIZE, ScreenSharePickerWindow } from '../windows' import { PhoneIslandController } from './PhoneIslandController' @@ -133,11 +134,16 @@ export class ScreenSharePickerController { s.appIcon && !s.appIcon.isEmpty() ? s.appIcon.toDataURL() : undefined, })) - const loadTimeout = setTimeout(() => { - Log.warning('SCREEN SHARE picker did not load') - settle() - }, PICKER_LOAD_TIMEOUT_MS) + let sourcesRequested = false + let loadTimeout: ReturnType | undefined + window.webContents.once('did-finish-load', () => { + if (sourcesRequested) return + loadTimeout = setTimeout(() => { + Log.warning('SCREEN SHARE picker did not load') + settle() + }, PICKER_LOAD_TIMEOUT_MS) + }) window.on('closed', () => { clearTimeout(loadTimeout) settle() @@ -146,6 +152,7 @@ export class ScreenSharePickerController { window.webContents.on('did-fail-load', () => settle()) pickerWindow.addListener(IPC_EVENTS.SCREEN_SHARE_PICKER_SOURCES, () => { + sourcesRequested = true clearTimeout(loadTimeout) pickerWindow.emit( IPC_EVENTS.SCREEN_SHARE_PICKER_SOURCES, @@ -184,8 +191,20 @@ export class ScreenSharePickerController { } catch (e) { Log.warning('SCREEN SHARE picker positioning error', e) } + if (process.platform === 'darwin') { + window.setVisibleOnAllWorkspaces(true, { + visibleOnFullScreen: true, + skipTransformProcessType: true, + }) + } window.show() - window.focus() + window.setAlwaysOnTop(true, 'screen-saver') + setTimeout(() => { + if (window.isDestroyed()) return + forceWindowFocus(window) + window.focus() + window.webContents.focus() + }, 50) } private destroyWindow() { From 8439c36a76fd161773f078d2c1c8175318ffd756 Mon Sep 17 00:00:00 2001 From: Antonio Date: Thu, 15 Oct 2026 17:20:53 +0200 Subject: [PATCH 3/3] fix(screen-share): start picker load timeout on window creation --- .../controllers/ScreenSharePickerController.ts | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/src/main/classes/controllers/ScreenSharePickerController.ts b/src/main/classes/controllers/ScreenSharePickerController.ts index c36da24e..6c24d392 100644 --- a/src/main/classes/controllers/ScreenSharePickerController.ts +++ b/src/main/classes/controllers/ScreenSharePickerController.ts @@ -14,7 +14,7 @@ import { PhoneIslandController } from './PhoneIslandController' type SettleRequest = (source?: DesktopCapturerSource) => void -const PICKER_LOAD_TIMEOUT_MS = 10000 +const PICKER_LOAD_TIMEOUT_MS = 30000 export class ScreenSharePickerController { static instance: ScreenSharePickerController @@ -134,16 +134,11 @@ export class ScreenSharePickerController { s.appIcon && !s.appIcon.isEmpty() ? s.appIcon.toDataURL() : undefined, })) - let sourcesRequested = false - let loadTimeout: ReturnType | undefined + const loadTimeout = setTimeout(() => { + Log.warning('SCREEN SHARE picker did not load') + settle() + }, PICKER_LOAD_TIMEOUT_MS) - window.webContents.once('did-finish-load', () => { - if (sourcesRequested) return - loadTimeout = setTimeout(() => { - Log.warning('SCREEN SHARE picker did not load') - settle() - }, PICKER_LOAD_TIMEOUT_MS) - }) window.on('closed', () => { clearTimeout(loadTimeout) settle() @@ -152,7 +147,6 @@ export class ScreenSharePickerController { window.webContents.on('did-fail-load', () => settle()) pickerWindow.addListener(IPC_EVENTS.SCREEN_SHARE_PICKER_SOURCES, () => { - sourcesRequested = true clearTimeout(loadTimeout) pickerWindow.emit( IPC_EVENTS.SCREEN_SHARE_PICKER_SOURCES,