Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(URLProtocol): support browser launch flat join room #772

Merged
merged 7 commits into from
Jun 29, 2021
7 changes: 5 additions & 2 deletions desktop/main-app/electron-builder.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,11 @@ mac:
extendInfo:
NSMicrophoneUsageDescription: To ensure normal use, flat-native needs access to your microphone.
NSCameraUsageDescription: To ensure normal use, flat-native needs access to your camera.
CFBundleURLSchemes:
- x-agora-flat-client
CFBundleURLTypes:
- CFBundleURLName: x-agora-flat-client
CFBundleURLSchemes:
- x-agora-flat-client
NSPrincipalClass: AtomApplication
target:
- dmg
- zip
Expand Down
3 changes: 1 addition & 2 deletions desktop/main-app/src/bootup/Init-app-listener.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { app } from "electron";
import closeAPP from "../utils/CloseAPP";
import { URLProtocol } from "../utils/URLProtocol";

export default () => {
const windowAllClosed = () => {
Expand All @@ -17,7 +16,7 @@ export default () => {

// Does not require sequential execution
// Just to avoid local variables polluting Context variables
[windowAllClosed, appQuit, URLProtocol].forEach(f => {
[windowAllClosed, appQuit].forEach(f => {
f();
});
};
113 changes: 113 additions & 0 deletions desktop/main-app/src/bootup/Init-url-protocol.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import runtime from "../utils/Runtime";
import { app } from "electron";
import closeAPP from "../utils/CloseAPP";
import { CustomSingleWindow, windowManager } from "../utils/WindowManager";
import { constants } from "flat-types";

export default async () => {
const protocol = new URLProtocolHandler({
active: () => {
// nothing...
},
joinRoom: (args, innerWindow) => {
if (args.has("roomUUID")) {
innerWindow.window.webContents.send("request-join-room", {
roomUUID: args.get("roomUUID"),
});
}
},
});

if (runtime.isMac) {
app.on("open-url", (event, url) => {
event.preventDefault();

protocol.execute(url);
});
}

// requestSingleInstanceLock must be called after ready
await app.whenReady();

if (runtime.isWin) {
// in any case, this must be called, otherwise second-instance will not be triggered
const lock = app.requestSingleInstanceLock();

const url = process.argv.slice().pop()!;

// act of opening by url protocol should not launch a new app (except for the first open)
if (url.startsWith("x-agora-flat-client://") && !lock) {
return closeAPP();
}

protocol.execute(url);

app.on("second-instance", (_event, command) => {
protocol.execute(command.pop()!);
});
}
};

class URLProtocolHandler {
public readonly handlers: ActionHandler;

public constructor(handlers: ActionHandler) {
this.handlers = Object.freeze(handlers);
}

public execute(url: string): void {
const actionInfo = this.getActionInfo(url);

if (actionInfo) {
URLProtocolHandler.focus()
.then(innerWindow => {
this.handlers[actionInfo.name](actionInfo.args, innerWindow);
})
.catch(() => {
// nothing..
});
}
}

private static async focus(): Promise<CustomSingleWindow> {
const innerWindow = await windowManager.getWindow(constants.WindowsName.Main, true);

const mainWindow = innerWindow.window;

if (mainWindow) {
if (mainWindow.isMinimized()) {
mainWindow.restore();
}
mainWindow.focus();
}

return innerWindow.didFinishLoad.then(() => innerWindow);
}

private getActionInfo(url: string): ActionInfo {
try {
const data = new URL(url);
const actionName = data.hostname as ActionNames;

if (!this.handlers[actionName]) {
return null;
}

return {
name: actionName,
args: data.searchParams,
};
} catch (_err) {
return null;
}
}
}

type ActionNames = "active" | "joinRoom";
type ActionHandler = {
[key in ActionNames]: (arg: URLSearchParams, innerWindow: CustomSingleWindow) => void;
};
type ActionInfo = {
name: ActionNames;
args: URLSearchParams;
} | null;
6 changes: 1 addition & 5 deletions desktop/main-app/src/bootup/Init-window.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
import { app } from "electron";
import { windowManager } from "../utils/WindowManager";

export default async () => {
await new Promise(resolve => {
app.on("ready", resolve);
});

export default () => {
app.allowRendererProcessReuse = false;

if (process.env.NODE_ENV === "development") {
Expand Down
2 changes: 2 additions & 0 deletions desktop/main-app/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@ import initMenus from "./bootup/Init-menus";
import intIPC from "./bootup/Init-ipc";
import initAppListen from "./bootup/Init-app-listener";
import initOtherListeners from "./bootup/Init-other";
import initURLProtocol from "./bootup/Init-url-protocol";

void bootstrap([
initEnv,
initURLProtocol,
initWindow,
initMenus,
intIPC,
Expand Down
83 changes: 0 additions & 83 deletions desktop/main-app/src/utils/URLProtocol.ts

This file was deleted.

59 changes: 38 additions & 21 deletions desktop/main-app/src/utils/WindowManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,17 @@ const defaultBrowserWindowOptions: BrowserWindowConstructorOptions = {
},
};

export class WindowManager {
private readonly wins: CustomWindows;
class RxSubject {
public constructor(
public readonly mainWindowCreated = new Subject(),
public readonly domReady = new Subject<string>(),
public readonly preloadLoad = new Subject<IpcMainEvent>(),
) {}
}

public constructor() {
this.wins = {};
export class WindowManager extends RxSubject {
public constructor(private readonly wins: CustomWindows = {}) {
super();
}

private createWindow(
Expand All @@ -45,18 +51,19 @@ export class WindowManager {
...windowOptions,
};

const window = new BrowserWindow({
...defaultBrowserWindowOptions,
...browserWindowOptions,
});

this.wins[options.name] = {
options,
window: new BrowserWindow({
...defaultBrowserWindowOptions,
...browserWindowOptions,
}),
window,
didFinishLoad: window.loadURL(options.url),
};

const innerWin = this.getWindow(options.name)!;

void innerWin.window.loadURL(options.url);

windowOpenDevTools(innerWin);

windowHookClose(innerWin);
Expand All @@ -71,12 +78,22 @@ export class WindowManager {
return innerWin;
}

public getWindow(name: constants.WindowsName): CustomSingleWindow | undefined {
return this.wins[name];
}

public getMainWindow(): CustomSingleWindow | undefined {
return this.wins[constants.WindowsName.Main];
public getWindow<W extends true>(
name: constants.WindowsName,
waiting: W,
): Promise<CustomSingleWindow>;
public getWindow(name: constants.WindowsName, waiting?: false): CustomSingleWindow | undefined;
public getWindow(
name: constants.WindowsName,
waiting?: boolean,
): Promise<CustomSingleWindow> | (CustomSingleWindow | undefined) {
if (waiting) {
return this.mainWindowCreated.toPromise().then(() => {
return this.wins[name]!;
});
} else {
return this.wins[name];
}
}

public createMainWindow(): CustomSingleWindow {
Expand All @@ -92,21 +109,20 @@ export class WindowManager {
},
);

const domReady = new Subject<string>();
const preloadLoad = new Subject<IpcMainEvent>();
this.mainWindowCreated.complete();

win.window.webContents.on("dom-ready", () => {
domReady.next("");
this.domReady.next("");
});

ipcMain.on("preload-load", event => {
preloadLoad.next(event);
this.preloadLoad.next(event);
});

// use the zip operator to solve the problem of not sending xx event after refreshing the page
// don’t worry about sending multiple times, because once is used in preload.ts
// link: https://www.learnrxjs.io/learn-rxjs/operators/combination/zip
zip(domReady, preloadLoad)
zip(this.domReady, this.preloadLoad)
.pipe(
mergeMap(([, event]) => {
event.sender.send("inject-agora-electron-sdk-addon");
Expand All @@ -125,6 +141,7 @@ export const windowManager = new WindowManager();
export type CustomSingleWindow = {
window: BrowserWindow;
options: WindowOptions;
didFinishLoad: Promise<void>;
};

type CustomWindows = {
Expand Down
Loading