diff --git a/electron-builder.json b/electron-builder.json index df4f28dd83..7891138eb0 100644 --- a/electron-builder.json +++ b/electron-builder.json @@ -18,7 +18,7 @@ "category": "public.app-category.productivity", "target": ["dmg", "pkg", "zip", "mas"], "icon": "build/icon.icns", - "bundleVersion": "26107", + "bundleVersion": "26108", "helperBundleId": "chat.rocket.electron.helper", "type": "distribution", "artifactName": "rocketchat-${version}-${os}.${ext}", diff --git a/package.json b/package.json index fd93ecb8a4..e7aa0172d7 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "productName": "Rocket.Chat", "name": "rocketchat", "description": "Official OSX, Windows, and Linux Desktop Clients for Rocket.Chat", - "version": "4.18.1", + "version": "4.18.2", "author": "Rocket.Chat Support ", "copyright": "© 2016-2026, Rocket.Chat", "homepage": "https://rocket.chat", diff --git a/src/app/PersistableValues.ts b/src/app/PersistableValues.ts index b66b4acb7a..99647cc215 100644 --- a/src/app/PersistableValues.ts +++ b/src/app/PersistableValues.ts @@ -160,6 +160,9 @@ export type PersistableValues = Omit< /** Current menu-bar default policy for Windows/Linux (auto-hide, Alt reveals). */ export const MENU_BAR_DEFAULT_REVISION = 1; +/** Mac App Store builds stored the internal video call window as off; turn it on once. */ +export const MAS_INTERNAL_VIDEO_CHAT_WINDOW_DEFAULT_REVISION = 1; + export const migrations = { '>=3.1.0': (before: PersistableValues_0_0_0): PersistableValues_3_1_0 => { const { currentServerUrl, ...rest } = before; @@ -189,7 +192,7 @@ export const migrations = { }), '>=3.8.4': (before: PersistableValues_3_8_1): PersistableValues_3_8_4 => ({ ...before, - isInternalVideoChatWindowEnabled: !process.mas, + isInternalVideoChatWindowEnabled: true, isAddNewServersEnabled: true, }), '>=3.8.7': (before: PersistableValues_3_8_4): PersistableValues_3_8_7 => ({ diff --git a/src/app/__tests__/PersistableValues.spec.ts b/src/app/__tests__/PersistableValues.spec.ts index a17b6003ff..1f6a0c97f9 100644 --- a/src/app/__tests__/PersistableValues.spec.ts +++ b/src/app/__tests__/PersistableValues.spec.ts @@ -235,7 +235,7 @@ describe('PersistableValues migrations', () => { setProcessMas(true); const mas384 = migrations['>=3.8.4'](win37 as any); - expect(mas384.isInternalVideoChatWindowEnabled).toBe(false); + expect(mas384.isInternalVideoChatWindowEnabled).toBe(true); }); it('applies remaining migration defaults through 4.15.0', () => { diff --git a/src/app/main/data.spec.ts b/src/app/main/data.spec.ts index 360651d747..95a1e3f7ae 100644 --- a/src/app/main/data.spec.ts +++ b/src/app/main/data.spec.ts @@ -1,7 +1,10 @@ import fs from 'fs'; import * as store from '../../store'; -import { MENU_BAR_DEFAULT_REVISION } from '../PersistableValues'; +import { + MAS_INTERNAL_VIDEO_CHAT_WINDOW_DEFAULT_REVISION, + MENU_BAR_DEFAULT_REVISION, +} from '../PersistableValues'; import { APP_SETTINGS_LOADED } from '../actions'; import { mergePersistableValues } from './data'; import { @@ -195,6 +198,117 @@ describe('mergePersistableValues', () => { }); }); + describe('Mac App Store internal video call window default', () => { + const processMasDescriptor = Object.getOwnPropertyDescriptor( + process, + 'mas' + ); + + const mockMasRevision = (revision: number) => { + (getPersistedMeta as jest.Mock).mockImplementation( + (key: string, fallback: unknown) => { + if (key === 'masInternalVideoChatWindowDefaultRevision') { + return revision; + } + if (key === 'menuBarDefaultRevision') { + return MENU_BAR_DEFAULT_REVISION; + } + return fallback; + } + ); + }; + + beforeEach(() => { + Object.defineProperty(process, 'mas', { + value: true, + writable: true, + configurable: true, + }); + mockSelect.mockReturnValue({ + ...mockInitialValues, + isInternalVideoChatWindowEnabled: false, + }); + }); + + afterEach(() => { + if (processMasDescriptor) { + Object.defineProperty(process, 'mas', processMasDescriptor); + } else { + delete (process as { mas?: boolean }).mas; + } + }); + + it('one-shot: turns the internal window on when the revision is stale', async () => { + mockMasRevision(0); + + await mergePersistableValues({}); + + expect(mockDispatch).toHaveBeenCalledWith({ + type: APP_SETTINGS_LOADED, + payload: expect.objectContaining({ + isInternalVideoChatWindowEnabled: true, + }), + }); + expect(setPersistedMeta).toHaveBeenCalledWith( + 'masInternalVideoChatWindowDefaultRevision', + MAS_INTERNAL_VIDEO_CHAT_WINDOW_DEFAULT_REVISION + ); + }); + + it('one-shot: keeps the user choice after the revision is applied', async () => { + mockMasRevision(MAS_INTERNAL_VIDEO_CHAT_WINDOW_DEFAULT_REVISION); + + await mergePersistableValues({}); + + expect(mockDispatch).toHaveBeenCalledWith({ + type: APP_SETTINGS_LOADED, + payload: expect.objectContaining({ + isInternalVideoChatWindowEnabled: false, + }), + }); + expect(setPersistedMeta).not.toHaveBeenCalled(); + }); + + it('one-shot: keeps an overridden-settings.json value', async () => { + mockMasRevision(0); + (fs.promises.readFile as jest.Mock).mockResolvedValueOnce( + JSON.stringify({ isInternalVideoChatWindowEnabled: false }) + ); + + await mergePersistableValues({}); + + expect(mockDispatch).toHaveBeenCalledWith({ + type: APP_SETTINGS_LOADED, + payload: expect.objectContaining({ + isInternalVideoChatWindowEnabled: false, + }), + }); + expect(setPersistedMeta).toHaveBeenCalledWith( + 'masInternalVideoChatWindowDefaultRevision', + MAS_INTERNAL_VIDEO_CHAT_WINDOW_DEFAULT_REVISION + ); + }); + + it('does nothing outside the Mac App Store build', async () => { + Object.defineProperty(process, 'mas', { + value: false, + writable: true, + configurable: true, + }); + mockMasRevision(0); + + await mergePersistableValues({}); + + expect(mockDispatch).toHaveBeenCalledWith({ + type: APP_SETTINGS_LOADED, + payload: expect.objectContaining({ + isInternalVideoChatWindowEnabled: false, + }), + }); + expect(setPersistedMeta).not.toHaveBeenCalled(); + }); + }); + describe('legacy localStorage migration', () => { it('should handle autohideMenu from localStorage', async () => { const localStorage = { diff --git a/src/app/main/data.ts b/src/app/main/data.ts index 47090a15b0..ae330db28f 100644 --- a/src/app/main/data.ts +++ b/src/app/main/data.ts @@ -7,7 +7,10 @@ import { logger } from '../../logging'; import { select, dispatch, watch } from '../../store'; import { getSystemCertificateStatus } from '../../systemCertificates'; import { normalizeNumber } from '../../ui/main/rootWindow'; -import { MENU_BAR_DEFAULT_REVISION } from '../PersistableValues'; +import { + MAS_INTERNAL_VIDEO_CHAT_WINDOW_DEFAULT_REVISION, + MENU_BAR_DEFAULT_REVISION, +} from '../PersistableValues'; import { APP_SETTINGS_LOADED } from '../actions'; import { selectPersistableValues } from '../selectors'; import { @@ -178,6 +181,34 @@ export const mergePersistableValues = async ( setPersistedMeta('menuBarDefaultRevision', MENU_BAR_DEFAULT_REVISION); } + // One-shot policy: Mac App Store builds used to force the internal video + // call window off. Turn it on once; a later user choice or an + // overridden-settings.json value still wins. + if (process.mas) { + const appliedMasVideoCallRevision = getPersistedMeta( + 'masInternalVideoChatWindowDefaultRevision', + 0 + ); + if ( + appliedMasVideoCallRevision < + MAS_INTERNAL_VIDEO_CHAT_WINDOW_DEFAULT_REVISION + ) { + const isOverridden = + 'isInternalVideoChatWindowEnabled' in userDataPersistableOverrides || + 'isInternalVideoChatWindowEnabled' in appAsarPersistableOverrides; + if (!isOverridden) { + values = { + ...values, + isInternalVideoChatWindowEnabled: true, + }; + } + setPersistedMeta( + 'masInternalVideoChatWindowDefaultRevision', + MAS_INTERNAL_VIDEO_CHAT_WINDOW_DEFAULT_REVISION + ); + } + } + const mergedOverrides: Record = { ...userDataOverriddenSettings, ...appAsarOverriddenSettings, diff --git a/src/i18n/de-DE.i18n.json b/src/i18n/de-DE.i18n.json index f9ebc4a36a..f1ea552891 100644 --- a/src/i18n/de-DE.i18n.json +++ b/src/i18n/de-DE.i18n.json @@ -243,8 +243,7 @@ }, "internalVideoChatWindow": { "title": "Öffnen Sie den Video-Chat mit dem Anwendungsfenster", - "description": "Wenn diese Option aktiviert ist, wird der Video-Chat im Anwendungsfenster anstelle des Standardbrowsers geöffnet. Allerdings wird für Google Meet und Jitsi die Bildschirmaufzeichnung in Electron-Anwendungen nicht unterstützt, daher werden sie unabhängig von dieser Einstellung immer im Browser geöffnet.", - "masDescription": "Diese Option ist deaktiviert, wenn sie aus dem Mac App Store installiert wird. Aus Sicherheitsgründen wird der Video-Chat standardmäßig über den Browser geöffnet." + "description": "Wenn diese Option aktiviert ist, wird der Video-Chat im Anwendungsfenster anstelle des Standardbrowsers geöffnet. Allerdings wird für Google Meet die Bildschirmaufzeichnung in Electron-Anwendungen nicht unterstützt, daher wird es unabhängig von dieser Einstellung immer im Browser geöffnet." }, "minimizeOnClose": { "title": "Beim Schließen minimieren", diff --git a/src/i18n/en.i18n.json b/src/i18n/en.i18n.json index c3a6d2bc42..65b7b35b91 100644 --- a/src/i18n/en.i18n.json +++ b/src/i18n/en.i18n.json @@ -372,8 +372,7 @@ }, "internalVideoChatWindow": { "title": "Video calls inside app", - "description": "Open video calls inside app window instead of browser. Google Meet and Jitsi calls can only open in browser.", - "masDescription": "This option is disabled when installed from the Mac App Store. For security reasons, video calls always open in the browser by default." + "description": "Open video calls inside app window instead of browser. Google Meet calls can only open in browser." }, "minimizeOnClose": { "title": "Minimize on close", diff --git a/src/i18n/es.i18n.json b/src/i18n/es.i18n.json index 200827337b..c277339ce3 100644 --- a/src/i18n/es.i18n.json +++ b/src/i18n/es.i18n.json @@ -262,8 +262,7 @@ }, "internalVideoChatWindow": { "title": "Abrir videollamada en la ventana de la aplicación", - "description": "Si está activado, la videollamada se abrirá en la ventana de la aplicación en lugar del navegador predeterminado. Sin embargo, para Google Meet y Jitsi, la grabación de pantalla no es compatible en aplicaciones Electron, por lo que siempre se abrirán en el navegador independientemente de esta configuración.", - "masDescription": "Esta opción está desactivada cuando se instala desde la Mac App Store; por razones de seguridad, las videollamadas se abrirán en el navegador de forma predeterminada." + "description": "Si está activado, la videollamada se abrirá en la ventana de la aplicación en lugar del navegador predeterminado. Sin embargo, para Google Meet, la grabación de pantalla no es compatible en aplicaciones Electron, por lo que siempre se abrirá en el navegador independientemente de esta configuración." }, "minimizeOnClose": { "title": "Minimizar al cerrar", diff --git a/src/i18n/fi.i18n.json b/src/i18n/fi.i18n.json index 7d6b4ea111..5e8e62159c 100644 --- a/src/i18n/fi.i18n.json +++ b/src/i18n/fi.i18n.json @@ -250,8 +250,7 @@ }, "internalVideoChatWindow": { "title": "Avaa videokeskustelu sovellusikkunaan", - "description": "Jos käytössä, videokeskustelu avautuu sovelluksen ikkunaan oletusselaimen sijaan. Kuitenkin Google Meet ja Jitsi -palveluissa ruuduntallennus ei ole tuettu Electron-sovelluksissa, joten ne avautuvat aina selaimessa tästä asetuksesta riippumatta.", - "masDescription": "Asetus ei ole käytössä, kun asennus on tehty Macin App Storesta. Suojaussyistä videokeskustelu avautuu oletusarvoisesti selaimeen." + "description": "Jos käytössä, videokeskustelu avautuu sovelluksen ikkunaan oletusselaimen sijaan. Kuitenkin Google Meet -palvelussa ruuduntallennus ei ole tuettu Electron-sovelluksissa, joten se avautuu aina selaimessa tästä asetuksesta riippumatta." }, "minimizeOnClose": { "title": "Pienennä suljettaessa", diff --git a/src/i18n/fr.i18n.json b/src/i18n/fr.i18n.json index 7d31801576..a588ae67eb 100644 --- a/src/i18n/fr.i18n.json +++ b/src/i18n/fr.i18n.json @@ -250,8 +250,7 @@ }, "internalVideoChatWindow": { "title": "Ouvrir le chat vidéo à l'aide de la fenêtre de l'application", - "description": "Si cette option est activée, le chat vidéo s'ouvrira dans la fenêtre de l'application au lieu du navigateur par défaut. Cependant, pour Google Meet et Jitsi, l'enregistrement d'écran n'est pas pris en charge dans les applications Electron, donc ils s'ouvriront toujours dans le navigateur, quel que soit ce paramètre.", - "masDescription": "Cette option est désactivée lorsqu'elle est installée à partir du Mac App Store, pour des raisons de sécurité, elle ouvrira le chat vidéo en utilisant le navigateur par défaut." + "description": "Si cette option est activée, le chat vidéo s'ouvrira dans la fenêtre de l'application au lieu du navigateur par défaut. Cependant, pour Google Meet, l'enregistrement d'écran n'est pas pris en charge dans les applications Electron, donc il s'ouvrira toujours dans le navigateur, quel que soit ce paramètre." }, "minimizeOnClose": { "title": "Minimiser à la fermeture", diff --git a/src/i18n/hu.i18n.json b/src/i18n/hu.i18n.json index 6bb6e7be33..ca8dd99469 100644 --- a/src/i18n/hu.i18n.json +++ b/src/i18n/hu.i18n.json @@ -276,8 +276,7 @@ }, "internalVideoChatWindow": { "title": "Videocsevegés megnyitása az alkalmazásablakban", - "description": "Ha engedélyezve van, akkor a videócsevegés az alkalmazás ablakában nyílik meg az alapértelmezett böngésző helyett. Azonban a Google Meet és a Jitsi esetében a képernyő rögzítése nem támogatott az Electron alkalmazásokban, így azok ettől a beállítástól függetlenül mindig a böngészőben nyílnak meg.", - "masDescription": "Ez a beállítás le van tiltva a Mac App Store-ból történő telepítéskor. Biztonsági okokból a videócsevegés alapértelmezetten mindig a böngészőben nyílik meg." + "description": "Ha engedélyezve van, akkor a videócsevegés az alkalmazás ablakában nyílik meg az alapértelmezett böngésző helyett. Azonban a Google Meet esetében a képernyő rögzítése nem támogatott az Electron alkalmazásokban, így az ettől a beállítástól függetlenül mindig a böngészőben nyílik meg." }, "minimizeOnClose": { "title": "Kis méret bezáráskor", diff --git a/src/i18n/no.i18n.json b/src/i18n/no.i18n.json index f0da524222..599f527eee 100644 --- a/src/i18n/no.i18n.json +++ b/src/i18n/no.i18n.json @@ -283,8 +283,7 @@ }, "internalVideoChatWindow": { "title": "Åpne videochat ved hjelp av applikasjonsvinduet", - "description": "Når innstilt, vil Videochat åpnes ved hjelp av et programvindu, ellers vil standardnettleseren brukes. Google Meet og Jitsi skjermopptak støttes ikke i Electron-applikasjoner, så denne konfigurasjonen endrer ikke anropsadferd, som åpnes i nettleseren.", - "masDescription": "Dette alternativet er deaktivert når det er installert fra Mac App Store, av sikkerhetsgrunner vil det åpne Video Chat ved å bruke nettleseren som standard." + "description": "Når innstilt, vil Videochat åpnes ved hjelp av et programvindu, ellers vil standardnettleseren brukes. Google Meet skjermopptak støttes ikke i Electron-applikasjoner, så denne konfigurasjonen endrer ikke anropsadferd, som åpnes i nettleseren." }, "minimizeOnClose": { "title": "Minimer på nært hold", diff --git a/src/i18n/pt-BR.i18n.json b/src/i18n/pt-BR.i18n.json index 4c59fb978f..1786599c73 100644 --- a/src/i18n/pt-BR.i18n.json +++ b/src/i18n/pt-BR.i18n.json @@ -257,8 +257,7 @@ }, "internalVideoChatWindow": { "title": "Abrir chat em video em uma janela da aplicação", - "description": "Se ativado, o Chat de Vídeo será aberto na janela do aplicativo em vez do navegador padrão. No entanto, para Google Meet e Jitsi, a gravação de tela não é suportada em aplicativos Electron, então eles sempre serão abertos no navegador independentemente desta configuração.", - "masDescription": "Esta opção esta desativada quando for instalado através da Mac App Store, por motivos de segurança o chat em video sera aberto usando o navegador por padrão." + "description": "Se ativado, o Chat de Vídeo será aberto na janela do aplicativo em vez do navegador padrão. No entanto, para Google Meet, a gravação de tela não é suportada em aplicativos Electron, então ele sempre será aberto no navegador independentemente desta configuração." }, "minimizeOnClose": { "title": "Minimizar ao fechar", diff --git a/src/i18n/ru.i18n.json b/src/i18n/ru.i18n.json index d4091e11e0..3a21b11ea6 100644 --- a/src/i18n/ru.i18n.json +++ b/src/i18n/ru.i18n.json @@ -244,8 +244,7 @@ }, "internalVideoChatWindow": { "title": "Открывать видеочат в окне приложения", - "description": "Если этот параметр включен, видеочат будет открываться в окне приложения вместо браузера по умолчанию. Однако для Google Meet и Jitsi запись экрана не поддерживается в приложениях Electron, поэтому они всегда будут открываться в браузере независимо от этой настройки.", - "masDescription": "Эта опция отключена при установке из Mac App Store, по соображениям безопасности окно видео чата будет открыто в браузере по умолчанию." + "description": "Если этот параметр включен, видеочат будет открываться в окне приложения вместо браузера по умолчанию. Однако для Google Meet запись экрана не поддерживается в приложениях Electron, поэтому он всегда будет открываться в браузере независимо от этой настройки." }, "minimizeOnClose": { "title": "Свернуть в трей при закрытии", diff --git a/src/i18n/sv.i18n.json b/src/i18n/sv.i18n.json index 7ccacdfbba..cfe8ca7e9b 100644 --- a/src/i18n/sv.i18n.json +++ b/src/i18n/sv.i18n.json @@ -271,8 +271,7 @@ }, "internalVideoChatWindow": { "title": "Öppna videochatt i programfönstret", - "description": "Om den är aktiverad öppnas videochatten i programmets fönster i stället för i standardwebbläsaren. För Google Meet och Jitsi stöds dock inte skärminspelning i Electron-applikationer, så de kommer alltid att öppnas i webbläsaren oavsett denna inställning.", - "masDescription": "Detta alternativ är inaktiverat när det installeras från Mac App Store. Av säkerhetsskäl öppnas Video Chat alltid i webbläsaren som standard." + "description": "Om den är aktiverad öppnas videochatten i programmets fönster i stället för i standardwebbläsaren. För Google Meet stöds dock inte skärminspelning i Electron-applikationer, så det kommer alltid att öppnas i webbläsaren oavsett denna inställning." }, "minimizeOnClose": { "title": "Minimera vid stängning", diff --git a/src/i18n/zh-CN.i18n.json b/src/i18n/zh-CN.i18n.json index 38af992475..dc4b66aad3 100644 --- a/src/i18n/zh-CN.i18n.json +++ b/src/i18n/zh-CN.i18n.json @@ -177,8 +177,7 @@ }, "internalVideoChatWindow": { "title": "在程序窗口中打开视频通话", - "description": "如果启用,视频通话将在应用程序窗口中打开,而不是在默认浏览器中打开。但是,对于Google Meet和Jitsi,Electron应用程序不支持屏幕录制,所以无论此设置如何,它们始终会在浏览器中打开。", - "masDescription": "从Mac App Store安装时此选项被禁用。出于安全原因,视频通话将始终默认在浏览器中打开。" + "description": "如果启用,视频通话将在应用程序窗口中打开,而不是在默认浏览器中打开。但是,对于Google Meet,Electron应用程序不支持屏幕录制,所以无论此设置如何,它始终会在浏览器中打开。" }, "transparentWindow": { "title": "透明窗口效果", diff --git a/src/injected.ts b/src/injected.ts index 8b52337996..22070eb40a 100644 --- a/src/injected.ts +++ b/src/injected.ts @@ -744,7 +744,6 @@ const start = async () => { ); window.open = (url, name, features = '') => { if ( - !process.mas && window.RocketChatDesktop.getInternalVideoChatWindowEnabled() && typeof url === 'string' && jitsiDomain.length > 0 && diff --git a/src/servers/preload/__tests__/internalVideoChatWindow.spec.ts b/src/servers/preload/__tests__/internalVideoChatWindow.spec.ts index 1a3bc7f650..a877c37f0e 100644 --- a/src/servers/preload/__tests__/internalVideoChatWindow.spec.ts +++ b/src/servers/preload/__tests__/internalVideoChatWindow.spec.ts @@ -73,16 +73,20 @@ describe('servers/preload/internalVideoChatWindow', () => { expect(invokeMock).not.toHaveBeenCalled(); }); - it('falls back to external open when running from MAS build', () => { + it('opens the internal window when running from MAS build', () => { safeSelectMock.mockReturnValue(true); setProcessMas(true); openInternalVideoChatWindow('https://chat.example', { - providerName: 'googlemeet', + providerName: 'jitsi', }); - expect(openExternalMock).toHaveBeenCalledWith('https://chat.example/'); - expect(invokeMock).not.toHaveBeenCalled(); + expect(invokeMock).toHaveBeenCalledWith( + 'video-call-window/open-window', + 'https://chat.example/', + { providerName: 'jitsi' } + ); + expect(openExternalMock).not.toHaveBeenCalled(); }); it('opens jitsi calls on the dedicated invoke path', () => { diff --git a/src/servers/preload/internalVideoChatWindow.ts b/src/servers/preload/internalVideoChatWindow.ts index b74095ffb9..2e3a244b72 100644 --- a/src/servers/preload/internalVideoChatWindow.ts +++ b/src/servers/preload/internalVideoChatWindow.ts @@ -21,7 +21,7 @@ export const openInternalVideoChatWindow = ( if (!allowedProtocols.includes(validUrl.protocol)) { return; } - if (!process.mas && getInternalVideoChatWindowEnabled()) { + if (getInternalVideoChatWindowEnabled()) { switch (options?.providerName) { case 'jitsi': // window.open(validUrl.href, 'Video Call', 'scrollbars=true'); diff --git a/src/settingsWindow/sections/VideoCallsSection.tsx b/src/settingsWindow/sections/VideoCallsSection.tsx index 853d53bea4..e4ecab146a 100644 --- a/src/settingsWindow/sections/VideoCallsSection.tsx +++ b/src/settingsWindow/sections/VideoCallsSection.tsx @@ -11,7 +11,7 @@ export const VideoCallsSection = () => ( {process.platform === 'win32' && } - {!process.mas && } + ); diff --git a/src/ui/components/SettingsView/features/InternalVideoChatWindow.tsx b/src/ui/components/SettingsView/features/InternalVideoChatWindow.tsx index 3a328681fd..796de8874b 100644 --- a/src/ui/components/SettingsView/features/InternalVideoChatWindow.tsx +++ b/src/ui/components/SettingsView/features/InternalVideoChatWindow.tsx @@ -39,14 +39,9 @@ export const InternalVideoChatWindow = ( ); diff --git a/src/videoCallWindow/ipc.ts b/src/videoCallWindow/ipc.ts index 95d679dd31..1d1e33a8d9 100644 --- a/src/videoCallWindow/ipc.ts +++ b/src/videoCallWindow/ipc.ts @@ -610,6 +610,52 @@ const setupWebviewHandlers = (webContents: WebContents) => { }); }; +/** + * Whether two URLs open the same conference: the same page, whatever the + * fragment. Unparseable URLs are never the same. + */ +export const isSameConferenceUrl = (a: string, b: string): boolean => { + try { + const first = new URL(a); + const second = new URL(b); + return ( + first.origin === second.origin && + first.pathname === second.pathname && + first.search === second.search + ); + } catch { + return false; + } +}; + +/** + * What the call window's webview shows now. It can have moved on in-page from + * the URL the window was opened for: the web client's conference start page + * replaces its own address with the conference's once the conference exists. + */ +const getVideoCallWebviewUrl = (): string | null => { + const host = videoCallWindow?.webContents; + if (!host) { + return null; + } + + const guest = webContents + .getAllWebContents() + .find((wc: WebContents) => wc.hostWebContents === host); + + return guest && !guest.isDestroyed() ? guest.getURL() : null; +}; + +/** + * Whether the open call window is already on the conference at `url`, by the + * URL it was opened for or by what it shows now. Reopening it would tear the + * call down and join it again, so the others in it see this user freeze. + */ +const isShowingConference = (url: string): boolean => + [activeCall?.url, getVideoCallWebviewUrl()].some( + (shown) => !!shown && isSameConferenceUrl(shown, url) + ); + // eslint-disable-next-line complexity const openVideoCallWindow = async ( _wc: WebContents, @@ -627,7 +673,7 @@ const openVideoCallWindow = async ( videoCallWindow && !videoCallWindow.isDestroyed() && !isVideoCallWindowDestroying && - activeCall?.url === url + isShowingConference(url) ) { console.log( 'Video call window: same conference already open, focusing existing window' diff --git a/src/videoCallWindow/main/ipc.main.spec.ts b/src/videoCallWindow/main/ipc.main.spec.ts index dd7a5389ee..ebd1245943 100644 --- a/src/videoCallWindow/main/ipc.main.spec.ts +++ b/src/videoCallWindow/main/ipc.main.spec.ts @@ -1319,4 +1319,108 @@ describe('videoCallWindow/ipc — PR #3359 hardening', () => { expect(guest.session.setPermissionRequestHandler).not.toHaveBeenCalled(); }); }); + + // ------------------------------------------------------------------------- + // open — the same conference focuses the open window instead of recreating it + // ------------------------------------------------------------------------- + describe('reopening the conference the window is already on', () => { + const startUrl = 'https://chat.example/conference/new?rid=room-1'; + const conferenceUrl = 'https://chat.example/conference/call-1'; + + /** The webview inside the open call window, showing `shown`. */ + const showInWebview = async (shown: string) => { + const { webContents } = (await import('electron')) as any; + const host = createdWindows[0].webContents; + webContents.getAllWebContents.mockReturnValue([ + { + hostWebContents: host, + getURL: jest.fn(() => shown), + isDestroyed: jest.fn(() => false), + }, + ]); + }; + + it('focuses the window opened for the same URL', async () => { + getServerUrlByWebContentsId.mockReturnValue('https://chat.example'); + const { openWindow } = await loadModule(); + await open(openWindow, makeCallerWc(1), conferenceUrl); + + await open(openWindow, makeCallerWc(1), conferenceUrl); + + expect(createdWindows).toHaveLength(1); + expect((createdWindows[0] as any).close).not.toHaveBeenCalled(); + expect((createdWindows[0] as any).focus).toHaveBeenCalled(); + }); + + // A call the user starts opens at the start page, which replaces its own + // address with the conference's once the conference exists — so the URL the + // window was opened for no longer names the call it is in. + it('focuses the window whose webview has moved in-page to the conference', async () => { + getServerUrlByWebContentsId.mockReturnValue('https://chat.example'); + const { openWindow } = await loadModule(); + await open(openWindow, makeCallerWc(1), startUrl); + await showInWebview(conferenceUrl); + + await open(openWindow, makeCallerWc(1), conferenceUrl); + + expect(createdWindows).toHaveLength(1); + expect((createdWindows[0] as any).close).not.toHaveBeenCalled(); + expect((createdWindows[0] as any).focus).toHaveBeenCalled(); + }); + + it('replaces the window for a different conference', async () => { + getServerUrlByWebContentsId.mockReturnValue('https://chat.example'); + const { openWindow } = await loadModule(); + await open(openWindow, makeCallerWc(1), startUrl); + await showInWebview(conferenceUrl); + + await open( + openWindow, + makeCallerWc(1), + 'https://chat.example/conference/call-2' + ); + + expect((createdWindows[0] as any).close).toHaveBeenCalled(); + expect(createdWindows).toHaveLength(2); + }); + }); + + describe('isSameConferenceUrl', () => { + it.each([ + [ + 'a different fragment', + 'https://meet.example/room#config.startWithAudioMuted=true', + 'https://meet.example/room', + true, + ], + [ + 'a different query', + 'https://chat.example/conference/new?rid=room-1', + 'https://chat.example/conference/new?rid=room-2', + false, + ], + [ + 'a different path', + 'https://chat.example/conference/call-1', + 'https://chat.example/conference/call-2', + false, + ], + [ + 'a different origin', + 'https://chat.example/conference/call-1', + 'https://other.example/conference/call-1', + false, + ], + [ + 'an unparseable URL', + 'not a url', + 'https://chat.example/conference/call-1', + false, + ], + ])('compares URLs with %s', async (_case, a, b, expected) => { + const { mod } = await loadModule(); + + expect(mod.isSameConferenceUrl(a, b)).toBe(expected); + }); + }); });