diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 5f16bb2b1..7b75931fe 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -12,3 +12,4 @@ ## Internals - Fixed the Dockerfile used when testing PRs. ([#1057](https://github.com/realm/realm-studio/pull/1057)) - Removing all existing and future unused locals. ([#1058](https://github.com/realm/realm-studio/pull/1058)) +- Refactored singleton windows. ([#1066](https://github.com/realm/realm-studio/pull/1066)) diff --git a/src/main/Application.ts b/src/main/Application.ts index 33997a16a..9980b6691 100644 --- a/src/main/Application.ts +++ b/src/main/Application.ts @@ -44,6 +44,10 @@ import { getDefaultMenuTemplate } from './MainMenu'; import { Updater } from './Updater'; import { WindowManager } from './WindowManager'; +interface ICloudAuthenticationWindow extends Electron.BrowserWindow { + accountPromise: Promise; +} + export class Application { public static sharedApplication = new Application(); @@ -172,7 +176,9 @@ export class Application { this.cloudManager.deauthenticate(); } - public async showConnectToServer(props: IConnectToServerWindowProps) { + public showConnectToServer( + props: IConnectToServerWindowProps, + ): Promise { const { window, existing } = this.windowManager.createWindow({ type: 'connect-to-server', props, @@ -181,17 +187,17 @@ export class Application { if (existing) { window.focus(); return Promise.resolve(); - } - - return new Promise(resolve => { - window.show(); - window.webContents.once('did-finish-load', () => { - resolve(); + } else { + return new Promise(resolve => { + window.show(); + window.webContents.once('did-finish-load', () => { + resolve(); + }); }); - }); + } } - public showGreeting() { + public showGreeting(): Promise { const { window, existing } = this.windowManager.createWindow({ type: 'greeting', props: {}, @@ -200,27 +206,27 @@ export class Application { if (existing) { window.focus(); return Promise.resolve(); - } - - return new Promise(resolve => { - // Save this for later - // Show the window, the first time its ready-to-show - window.once('ready-to-show', () => { - window.show(); - resolve(); - }); - // Check for updates, every time the contents has loaded - window.webContents.on('did-finish-load', () => { - this.updater.checkForUpdates(true); - this.cloudManager.refresh(); - }); - this.updater.addListeningWindow(window); - this.cloudManager.addListeningWindow(window); - window.once('close', () => { - this.updater.removeListeningWindow(window); - this.cloudManager.removeListeningWindow(window); + } else { + return new Promise(resolve => { + // Save this for later + // Show the window, the first time its ready-to-show + window.once('ready-to-show', () => { + window.show(); + resolve(); + }); + // Check for updates, every time the contents has loaded + window.webContents.on('did-finish-load', () => { + this.updater.checkForUpdates(true); + this.cloudManager.refresh(); + }); + this.updater.addListeningWindow(window); + this.cloudManager.addListeningWindow(window); + window.once('close', () => { + this.updater.removeListeningWindow(window); + this.cloudManager.removeListeningWindow(window); + }); }); - }); + } } public showOpenLocalRealm() { @@ -279,7 +285,7 @@ export class Application { }); } - public showRealmBrowser(props: IRealmBrowserWindowProps) { + public showRealmBrowser(props: IRealmBrowserWindowProps): Promise { const { window, existing } = this.windowManager.createWindow({ type: 'realm-browser', props, @@ -288,21 +294,23 @@ export class Application { if (existing) { window.focus(); return Promise.resolve(); - } - - return new Promise(resolve => { - // Set the represented filename - if (process.platform === 'darwin' && props.realm.mode === 'local') { - window.setRepresentedFilename(props.realm.path); - } - window.show(); - window.webContents.once('did-finish-load', () => { - resolve(); + } else { + return new Promise(resolve => { + // Set the represented filename + if (process.platform === 'darwin' && props.realm.mode === 'local') { + window.setRepresentedFilename(props.realm.path); + } + window.show(); + window.webContents.once('did-finish-load', () => { + resolve(); + }); }); - }); + } } - public showServerAdministration(props: IServerAdministrationWindowProps) { + public showServerAdministration( + props: IServerAdministrationWindowProps, + ): Promise { const { window, existing } = this.windowManager.createWindow({ type: 'server-administration', props, @@ -311,72 +319,74 @@ export class Application { if (existing) { window.focus(); return Promise.resolve(); - } + } else { + return new Promise(resolve => { + window.show(); + window.webContents.once('did-finish-load', () => { + resolve(); + }); - return new Promise(resolve => { - window.show(); - window.webContents.once('did-finish-load', () => { - resolve(); + if (props.isCloudTenant) { + this.cloudManager.addListeningWindow(window); + window.once('close', () => { + this.cloudManager.removeListeningWindow(window); + }); + } }); - - if (props.isCloudTenant) { - this.cloudManager.addListeningWindow(window); - window.once('close', () => { - this.cloudManager.removeListeningWindow(window); - }); - } - }); + } } public showCloudAuthentication( props: ICloudAuthenticationWindowProps, resolveUser: boolean = false, ): Promise { - const { window, existing } = this.windowManager.createWindow({ + const { window, existing } = this.windowManager.createWindow< + ICloudAuthenticationWindow + >({ type: 'cloud-authentication', props, }); - let authPromise: Promise; - if (existing) { + if (existing && window.accountPromise) { window.focus(); - // authPromise is set in the else clause - authPromise = (window as any).authPromise; - } else { - // Hacky way to avoid recreating the auth promise every time the window is focused. - (window as any).authPromise = authPromise = new Promise< - raas.user.IAccountResponse - >((resolve, reject) => { - const listener = (status: ICloudStatus) => { - if (status.kind === 'authenticated') { - this.cloudManager.removeListener(listener); - resolve(status.account); - // Close the window once we're authenticated - window.close(); - } else if (status.kind === 'error') { + return window.accountPromise; + } else if (!existing) { + // Saving the account promise on the window to avoid recreating it every time the window is focused. + window.accountPromise = new Promise( + (resolve, reject) => { + const listener = (status: ICloudStatus) => { + if (status.kind === 'authenticated') { + this.cloudManager.removeListener(listener); + resolve(status.account); + // Close the window once we're authenticated + window.close(); + } else if (status.kind === 'error') { + this.cloudManager.removeListener(listener); + reject(new Error(status.message)); + } + }; + this.cloudManager.addListener(listener); + // Reject the promise if the window is closed before cloud status turns authenticated + window.once('close', () => { + // We need a timeout here, because the close event fires before the cloud status updates + reject(new Error('Window was closed instead of authenticating')); this.cloudManager.removeListener(listener); - reject(new Error(status.message)); - } - }; - this.cloudManager.addListener(listener); - // Reject the promise if the window is closed before cloud status turns authenticated - window.once('close', () => { - // We need a timeout here, because the close event fires before the cloud status updates - reject(new Error('Window was closed instead of authenticating')); - this.cloudManager.removeListener(listener); - this.cloudManager.abortPendingGitHubAuthentications(); - }); - // If resolveUser is false - we resolve the promise as soon as the window loads - if (!resolveUser) { - window.webContents.once('did-finish-load', () => { - resolve(); + this.cloudManager.abortPendingGitHubAuthentications(); }); - } - window.show(); - }); + // If resolveUser is false - we resolve the promise as soon as the window loads + if (!resolveUser) { + window.webContents.once('did-finish-load', () => { + resolve(); + }); + } + window.show(); + }, + ); + // Return the newly created promise + return window.accountPromise; + } else { + throw new Error('Expected existing window to have an account promise'); } - - return authPromise; } public checkForUpdates() { diff --git a/src/main/WindowManager.ts b/src/main/WindowManager.ts index dab2a3bcf..581058e0a 100644 --- a/src/main/WindowManager.ts +++ b/src/main/WindowManager.ts @@ -22,7 +22,11 @@ import * as path from 'path'; import * as url from 'url'; import { store } from '../store'; -import { getWindowOptions, IWindowConstructorOptions } from '../windows/Window'; +import { + getSingletonKey, + getWindowOptions, + IWindowConstructorOptions, +} from '../windows/Window'; import { WindowOptions, WindowType } from '../windows/WindowOptions'; export interface IEventListenerCallbacks { @@ -34,7 +38,7 @@ export interface IEventListenerCallbacks { interface IWindowHandle { window: Electron.BrowserWindow; type: string; - uniqueId: string | undefined; + singletonKey: string | undefined; } const isDevelopment = process.env.NODE_ENV === 'development'; @@ -47,31 +51,32 @@ function getRendererHtmlPath() { return path.resolve(__dirname, indexPath); } +interface ICreatedWindow { + // The existing or newly created window object + window: W; + // If true a window of the same type and singleton key already existed + existing: boolean; +} + export class WindowManager { public windows: IWindowHandle[] = []; - public createWindow( + /** + * Either creates a new or returns an existing window depending on the implementation of the getSingletonKey function + * defined by the window type and the props provided as argument. + */ + public createWindow( options: WindowOptions, - ): { window: BrowserWindow; existing: boolean } { - let uniqueId = ''; - switch (options.type) { - case 'realm-browser': - uniqueId = options.props.realm.path; - break; - case 'server-administration': - uniqueId = options.props.user.server; - break; - } - + ): ICreatedWindow { + // Generate a singleton key + const singletonKey = getSingletonKey(options); + // Find a window of the same type and unique id const existing = this.windows.find( - w => w.type === options.type && w.uniqueId === uniqueId, + w => w.type === options.type && w.singletonKey === singletonKey, ); - + // Return the window if another window of the same type and singleton key exists if (existing) { - return { - window: existing.window, - existing: true, - }; + return { window: existing.window as W, existing: true }; } // Get the window options that are default for this type of window @@ -127,11 +132,11 @@ export class WindowManager { }); // Construct the window - const window = new BrowserWindow(windowOptions); + const window = new BrowserWindow(windowOptions) as W; this.windows.push({ window, type: options.type, - uniqueId, + singletonKey, }); // If the window should maximize - let's maximize it when it gets shown @@ -221,20 +226,17 @@ export class WindowManager { }); }); - return { - window, - existing: false, - }; + return { window, existing: false }; } - public async closeAllWindows(): Promise<{}> { - return Promise.all( - // Create a new array as closing the windows will remove them from the + public async closeAllWindows(): Promise { + await Promise.all( + // Creates a new array using the mapping as closing the windows will remove them from the // this.windows collection this.windows .map(handle => handle.window) .map(window => { - return new Promise(resolve => { + return new Promise(resolve => { window.once('closed', resolve); window.close(); }); @@ -242,24 +244,24 @@ export class WindowManager { ); } + /** + * Gets the window options from the Electron store + */ + private getWindowOptions(type: WindowType) { + return store.getWindowOptions(type); + } + /** * Saves options that should be passed to windows of this type when created in the future. * Use this to remember the position or other state of the windows between instances. */ - public setWindowOptions( + private setWindowOptions( type: WindowType, options: IWindowConstructorOptions, ) { store.setWindowOptions(type, options); } - /** - * Gets the window options from the Electron store - */ - public getWindowOptions(type: WindowType) { - return store.getWindowOptions(type); - } - private getDesiredDisplay() { const desiredDisplayString = process.env.DISPLAY; if (typeof desiredDisplayString === 'string') { diff --git a/src/windows/RealmBrowserWindow.tsx b/src/windows/RealmBrowserWindow.tsx index 0180e5ecd..203785d3e 100644 --- a/src/windows/RealmBrowserWindow.tsx +++ b/src/windows/RealmBrowserWindow.tsx @@ -19,7 +19,11 @@ import { URL } from 'url'; import { ImportFormat } from '../services/data-importer'; -import { ISyncedRealmToLoad, RealmToLoad } from '../utils/realms'; +import { + ISyncedRealmToLoad, + RealmLoadingMode, + RealmToLoad, +} from '../utils/realms'; import { IWindow } from './Window'; @@ -57,6 +61,14 @@ export const RealmBrowserWindow: IWindow = { // TODO: Fix the props for this to include a type m => m.RealmBrowser as any, ), + getSingletonKey: (props: IRealmBrowserWindowProps) => { + const { realm } = props; + if (realm.mode === RealmLoadingMode.Synced) { + return `${realm.user.server}:${realm.path}`; + } else { + return realm.path; + } + }, getTrackedProperties: (props: IRealmBrowserWindowProps) => ({ mode: props.realm.mode, }), diff --git a/src/windows/ServerAdministrationWindow.tsx b/src/windows/ServerAdministrationWindow.tsx index cf4f92b2f..d93540173 100644 --- a/src/windows/ServerAdministrationWindow.tsx +++ b/src/windows/ServerAdministrationWindow.tsx @@ -40,6 +40,8 @@ export const ServerAdministrationWindow: IWindow = { // TODO: Fix the props for this to include a type m => m.ServerAdministration as any, ), + getSingletonKey: (props: IServerAdministrationWindowProps) => + props.user.server, getTrackedProperties: (props: IServerAdministrationWindowProps) => ({ url: props.user.server, }), diff --git a/src/windows/Window.tsx b/src/windows/Window.tsx index 2c6cdce5a..8ae7ddbc9 100644 --- a/src/windows/Window.tsx +++ b/src/windows/Window.tsx @@ -29,6 +29,7 @@ export interface IWindow { getWindowOptions( props: WindowProps, ): Partial; + getSingletonKey?(props: WindowProps): string | undefined; getTrackedProperties(props: WindowProps): { [key: string]: string }; } @@ -72,3 +73,13 @@ export function getWindowOptions({ const WindowClass = getWindowClass(type); return WindowClass.getWindowOptions(props); } + +export function getSingletonKey({ + type, + props, +}: WindowOptions): string | undefined { + const WindowClass = getWindowClass(type); + return WindowClass.getSingletonKey + ? WindowClass.getSingletonKey(props) + : undefined; +}