Encapsulated the external modal from spawning and administrating windows

master
WolverinDEV 2021-03-20 15:10:16 +01:00
parent 3d04fa3f0d
commit 0b5f519735
48 changed files with 830 additions and 685 deletions

View File

@ -1,3 +0,0 @@
window.__native_client_init_shared(__webpack_require__);
import "tc-shared/ui/react-elements/external-modal/PopoutEntrypoint";

View File

@ -0,0 +1,4 @@
window.__native_client_init_shared(__webpack_require__);
import "../AppMain.scss";
import "tc-shared/entry-points/MainApp";

View File

@ -1,4 +1,2 @@
window.__native_client_init_shared(__webpack_require__);
import "./index.scss";
import "tc-shared/main";
import "tc-shared/entry-points/ModalWindow";

View File

@ -10,7 +10,7 @@ export default class implements ApplicationLoader {
function: async taskId => {
await loadManifest();
const entryChunk = getUrlParameter("chunk");
const entryChunk = getUrlParameter("loader-chunk");
if(!entryChunk) {
loader.critical_error("Missing entry chunk parameter");
throw "Missing entry chunk parameter";

View File

@ -24,7 +24,7 @@ export let config: Config;
export type Task = {
name: string,
priority: number, /* tasks with the same priority will be executed in sync */
function: () => Promise<void>
function: (taskId: number) => Promise<void>
};
export enum Stage {
/*
@ -86,4 +86,5 @@ export type SourcePath = string | DependSource | string[];
export type ErrorHandler = (message: string, detail: string) => void;
export function critical_error(message: string, detail?: string);
export function critical_error_handler(handler?: ErrorHandler, override?: boolean);
export function hide_overlay();
export function hide_overlay();
export function setCurrentTaskName(taskId: number, name: string);

6
package-lock.json generated
View File

@ -19305,9 +19305,9 @@
}
},
"webpack-svg-sprite-generator": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/webpack-svg-sprite-generator/-/webpack-svg-sprite-generator-5.0.2.tgz",
"integrity": "sha512-i3b4aKgy2VZI7uYYEteKvnv3+mRtG+UfFSCYvnvoGs4dLJ++pDwbGGZL1XaYXN8qbePsHD88JQEvhfkChtx3aw==",
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/webpack-svg-sprite-generator/-/webpack-svg-sprite-generator-5.0.4.tgz",
"integrity": "sha512-j+Bl44VoF/Jv0pz0qfU/zeomoVUmWWZ0CGpaHdLH7xwHiCK4xCB2E+xaaSYSDRxoyNZNJEKESOIX+BzKCW4QYg==",
"dev": true
},
"webrtc-adapter": {

View File

@ -13,7 +13,8 @@
"webpack-web": "webpack --config webpack-web.config.js",
"webpack-client": "webpack --config webpack-client.config.js",
"generate-i18n-gtranslate": "node shared/generate_i18n_gtranslate.js",
"dev-server": "npm run compile-project-base && webpack serve --config webpack-web.config.js"
"dev-server": "npm run compile-project-base && webpack serve --config webpack-web.config.js",
"dev-server-client": "npm run compile-project-base && webpack serve --config webpack-client.config.js"
},
"author": "TeaSpeak (WolverinDEV)",
"license": "ISC",
@ -85,7 +86,7 @@
"webpack-bundle-analyzer": "^3.6.1",
"webpack-cli": "^4.5.0",
"webpack-dev-server": "^3.11.2",
"webpack-svg-sprite-generator": "^5.0.2",
"webpack-svg-sprite-generator": "^5.0.4",
"zip-webpack-plugin": "^4.0.1"
},
"repository": {

View File

@ -2,7 +2,6 @@
@import "mixin";
:global {
.modal {
color: #999999; /* base color */

View File

@ -1,25 +0,0 @@
import * as loader from "tc-loader";
import {Stage} from "tc-loader";
import * as ipc from "./ipc/BrowserIPC";
import * as i18n from "./i18n/localize";
import "./proto";
console.error("Hello World from devel main");
loader.register_task(Stage.JAVASCRIPT_INITIALIZING, {
name: "setup",
priority: 10,
function: async () => {
await i18n.initialize();
ipc.setupIpcHandler();
}
});
loader.register_task(Stage.LOADED, {
name: "invoke",
priority: 10,
function: async () => {
}
});

View File

@ -0,0 +1 @@
import "../main";

View File

@ -0,0 +1 @@
import "../ui/react-elements/modal/external/renderer/EntryPoint";

View File

@ -316,7 +316,7 @@ export function select_translation(repository: TranslationRepository, entry: Rep
}
/* ATTENTION: This method is called before most other library initialisations! */
export async function initialize() {
export async function initializeI18N() {
const rcfg = config.repository_config(); /* initialize */
const cfg = config.translation_config();

View File

@ -59,7 +59,7 @@ assertMainApplication();
let preventWelcomeUI = false;
async function initialize() {
try {
await i18n.initialize();
await i18n.initializeI18N();
} catch(error) {
console.error(tr("Failed to initialized the translation system!\nError: %o"), error);
loader.critical_error("Failed to setup the translation system");

View File

@ -275,16 +275,10 @@ export namespace AppParameters {
description: "Peer address of the apps core",
};
export const KEY_MODAL_IDENTITY_CODE: RegistryKey<string> = {
key: "modal-identify",
export const KEY_MODAL_IPC_CHANNEL: RegistryKey<string> = {
key: "modal-channel",
valueType: "string",
description: "An authentication code used to register the new process as the modal"
};
export const KEY_MODAL_TARGET: RegistryKey<string> = {
key: "modal-target",
valueType: "string",
description: "Target modal unique id which should be loaded"
description: "The modal IPC channel id for communication with the controller"
};
export const KEY_LOAD_DUMMY_ERROR: ValuedRegistryKey<boolean> = {

View File

@ -1,5 +1,6 @@
import {ReactComponentBase} from "tc-shared/ui/react-elements/ReactComponentBase";
import * as React from "react";
import {joinClassList} from "tc-shared/ui/react-elements/Helper";
const cssStyle = require("./Button.scss");
@ -21,12 +22,14 @@ export interface ButtonState {
disabled?: boolean
}
export class Button extends ReactComponentBase<ButtonProperties, ButtonState> {
protected defaultState(): ButtonState {
return {
export class Button extends React.Component<ButtonProperties, ButtonState> {
constructor(props) {
super(props);
this.state = {
disabled: undefined
};
}
}
};
render() {
if(this.props.hidden)
@ -34,7 +37,7 @@ export class Button extends ReactComponentBase<ButtonProperties, ButtonState> {
return (
<button
className={this.classList(
className={joinClassList(
cssStyle.button,
cssStyle["color-" + this.props.color] || cssStyle["color-default"],
cssStyle["type-" + this.props.type] || cssStyle["type-normal"],

View File

@ -1,169 +0,0 @@
import {LogCategory, logDebug, logTrace} from "../../../log";
import * as ipc from "../../../ipc/BrowserIPC";
import {ChannelMessage} from "../../../ipc/BrowserIPC";
import {Registry} from "tc-events";
import {
EventControllerBase,
kPopoutIPCChannelId,
Popout2ControllerMessages,
PopoutIPCMessage
} from "../../../ui/react-elements/external-modal/IPCMessage";
import {ModalEvents, ModalOptions, ModalState} from "../../../ui/react-elements/ModalDefinitions";
import {guid} from "tc-shared/crypto/uid";
import {ModalInstanceController, ModalInstanceEvents} from "tc-shared/ui/react-elements/modal/Definitions";
export abstract class AbstractExternalModalController extends EventControllerBase<"controller"> implements ModalInstanceController {
public readonly modalType: string;
public readonly constructorArguments: any[];
private readonly modalEvents: Registry<ModalInstanceEvents>;
private modalState: ModalState = ModalState.DESTROYED;
private readonly documentUnloadListener: () => void;
private callbackWindowInitialized: (error?: string) => void;
protected constructor(modalType: string, constructorArguments: any[]) {
super(guid());
this.modalType = modalType;
this.constructorArguments = constructorArguments;
this.modalEvents = new Registry<ModalInstanceEvents>();
this.ipcChannel = ipc.getIpcInstance().createChannel(kPopoutIPCChannelId);
this.ipcChannel.messageHandler = this.handleIPCMessage.bind(this);
this.documentUnloadListener = () => this.destroy();
}
getOptions(): Readonly<ModalOptions> {
return {}; /* FIXME! */
}
getEvents(): Registry<ModalInstanceEvents> {
return this.modalEvents;
}
getState(): ModalState {
return this.modalState;
}
protected abstract spawnWindow() : Promise<boolean>;
protected abstract focusWindow() : void;
protected abstract destroyWindow() : void;
async show() {
if(this.modalState === ModalState.SHOWN) {
this.focusWindow();
return;
}
this.modalState = ModalState.SHOWN;
if(!await this.spawnWindow()) {
this.modalState = ModalState.DESTROYED;
throw tr("failed to create window");
}
try {
await new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
this.callbackWindowInitialized = undefined;
reject("window haven't called back");
}, 15000);
this.callbackWindowInitialized = error => {
this.callbackWindowInitialized = undefined;
clearTimeout(timeout);
error ? reject(error) : resolve();
};
});
} catch (e) {
this.modalState = ModalState.DESTROYED;
if(__build.mode !== "debug") {
/* do not destroy the window in debug mode in order to debug what happened */
this.doDestroyWindow();
}
throw e;
}
window.addEventListener("unload", this.documentUnloadListener);
this.modalEvents.fire("notify_open");
}
private doDestroyWindow() {
this.destroyWindow();
window.removeEventListener("beforeunload", this.documentUnloadListener);
}
async hide() {
if(this.modalState == ModalState.DESTROYED || this.modalState === ModalState.HIDDEN)
return;
this.doDestroyWindow();
this.modalState = ModalState.HIDDEN;
this.modalEvents.fire("notify_close");
}
destroy() {
if(this.modalState === ModalState.DESTROYED) {
return;
}
this.doDestroyWindow();
if(this.ipcChannel) {
ipc.getIpcInstance().deleteChannel(this.ipcChannel);
}
this.destroyIPC();
this.modalState = ModalState.DESTROYED;
this.modalEvents.fire("notify_destroy");
}
protected handleWindowClosed() {
/* no other way currently */
this.destroy();
}
protected handleIPCMessage(remoteId: string, broadcast: boolean, message: ChannelMessage) {
if(!broadcast && remoteId !== this.ipcRemotePeerId) {
logDebug(LogCategory.IPC, tr("Received direct IPC message for popout controller from unknown source: %s"), remoteId);
return;
}
this.handleTypedIPCMessage(remoteId, broadcast, message.type as any, message.data);
}
protected handleTypedIPCMessage<T extends Popout2ControllerMessages>(remoteId: string, isBroadcast: boolean, type: T, payload: PopoutIPCMessage[T]) {
super.handleTypedIPCMessage(remoteId, isBroadcast, type, payload);
if(type === "hello-popout") {
const messageHello = payload as PopoutIPCMessage["hello-popout"];
if(messageHello.authenticationCode !== this.ipcAuthenticationCode) {
/* most likely not for us */
return;
}
if(this.ipcRemotePeerId) {
logTrace(LogCategory.IPC, tr("Modal popout slave changed from %s to %s. Side reload?"), this.ipcRemotePeerId, remoteId);
/* TODO: Send a good by to the old modal */
}
this.ipcRemotePeerId = remoteId;
logTrace(LogCategory.IPC, "Received Hello World from popup (peer id %s) with version %s (expected %s).", remoteId, messageHello.version, __build.version);
if(messageHello.version !== __build.version) {
this.sendIPCMessage("hello-controller", { accepted: false, message: tr("version miss match") });
if(this.callbackWindowInitialized) {
this.callbackWindowInitialized(tr("version miss match"));
this.callbackWindowInitialized = undefined;
}
return;
}
if(this.callbackWindowInitialized) {
this.callbackWindowInitialized();
this.callbackWindowInitialized = undefined;
}
this.sendIPCMessage("hello-controller", { accepted: true, constructorArguments: this.constructorArguments });
}
}
}

View File

@ -1,47 +0,0 @@
import {IPCChannel} from "../../../ipc/BrowserIPC";
export const kPopoutIPCChannelId = "popout-channel";
export interface PopoutIPCMessage {
"hello-popout": { version: string, authenticationCode: string },
"hello-controller": { accepted: boolean, message?: string, constructorArguments?: any[] },
"invoke-modal-action": {
action: "close" | "minimize"
}
}
export type Controller2PopoutMessages = "hello-controller";
export type Popout2ControllerMessages = "hello-popout" | "invoke-modal-action";
export interface SendIPCMessage {
"controller": Controller2PopoutMessages;
"popout": Popout2ControllerMessages;
}
export interface ReceivedIPCMessage {
"controller": Popout2ControllerMessages;
"popout": Controller2PopoutMessages;
}
export abstract class EventControllerBase<Type extends "controller" | "popout"> {
protected readonly ipcAuthenticationCode: string;
protected ipcRemotePeerId: string;
protected ipcChannel: IPCChannel;
protected constructor(ipcAuthenticationCode: string) {
this.ipcAuthenticationCode = ipcAuthenticationCode;
}
protected sendIPCMessage<T extends SendIPCMessage[Type]>(type: T, payload: PopoutIPCMessage[T]) {
this.ipcChannel.sendMessage(type, payload, this.ipcRemotePeerId);
}
protected handleTypedIPCMessage<T extends ReceivedIPCMessage[Type]>(remoteId: string, isBroadcast: boolean, type: T, payload: PopoutIPCMessage[T]) {
}
protected destroyIPC() {
this.ipcChannel = undefined;
this.ipcRemotePeerId = undefined;
}
}

View File

@ -1,89 +0,0 @@
import {getIpcInstance as getIPCInstance} from "../../../ipc/BrowserIPC";
import {AppParameters} from "../../../settings";
import {
Controller2PopoutMessages,
EventControllerBase, kPopoutIPCChannelId,
PopoutIPCMessage,
} from "../../../ui/react-elements/external-modal/IPCMessage";
let controller: PopoutController;
export function getPopoutController() {
if(!controller) {
controller = new PopoutController();
}
return controller;
}
class PopoutController extends EventControllerBase<"popout"> {
private constructorArguments: any[];
private callbackControllerHello: (accepted: boolean | string) => void;
constructor() {
super(AppParameters.getValue(AppParameters.KEY_MODAL_IDENTITY_CODE, "invalid"));
this.ipcChannel = getIPCInstance().createChannel(kPopoutIPCChannelId);
this.ipcChannel.messageHandler = (sourcePeerId, broadcast, message) => {
this.handleTypedIPCMessage(sourcePeerId, broadcast, message.type as any, message.data);
};
}
getConstructorArguments() : any[] {
return this.constructorArguments;
}
async initialize() {
this.sendIPCMessage("hello-popout", { version: __build.version, authenticationCode: this.ipcAuthenticationCode });
await new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
this.callbackControllerHello = undefined;
reject("controller haven't called back");
}, 5000);
this.callbackControllerHello = result => {
this.callbackControllerHello = undefined;
clearTimeout(timeout);
if(typeof result === "string") {
reject(result);
} else if(!result) {
reject();
} else {
resolve();
}
};
});
}
protected handleTypedIPCMessage<T extends Controller2PopoutMessages>(remoteId: string, isBroadcast: boolean, type: T, payload: PopoutIPCMessage[T]) {
super.handleTypedIPCMessage(remoteId, isBroadcast, type, payload);
switch (type) {
case "hello-controller": {
const tpayload = payload as PopoutIPCMessage["hello-controller"];
this.ipcRemotePeerId = remoteId;
console.log("Received Hello World from controller (peer id %s). Window instance accepted: %o", this.ipcRemotePeerId, tpayload.accepted);
if(!this.callbackControllerHello) {
return;
}
this.constructorArguments = tpayload.constructorArguments;
this.callbackControllerHello(tpayload.accepted ? true : tpayload.message || false);
break;
}
default:
console.warn("Received unknown message type from controller: %s", type);
return;
}
}
doClose() {
this.sendIPCMessage("invoke-modal-action", { action: "close" });
}
doMinimize() {
this.sendIPCMessage("invoke-modal-action", { action: "minimize" });
}
}

View File

@ -1,89 +0,0 @@
import * as loader from "tc-loader";
import * as ipc from "../../../ipc/BrowserIPC";
import * as i18n from "../../../i18n/localize";
import {Stage} from "tc-loader";
import {AbstractModal} from "../../../ui/react-elements/ModalDefinitions";
import {AppParameters} from "../../../settings";
import {getPopoutController} from "./PopoutController";
import {setupJSRender} from "../../../ui/jsrender";
import {findRegisteredModal} from "tc-shared/ui/react-elements/modal/Registry";
import {ModalRenderer} from "tc-shared/ui/react-elements/external-modal/ModalRenderer";
import {constructAbstractModalClass} from "tc-shared/ui/react-elements/modal/Definitions";
import "../../../file/RemoteAvatars";
import "../../../file/RemoteIcons";
let modalRenderer: ModalRenderer;
let modalInstance: AbstractModal;
let modalClass: new (...args: any[]) => AbstractModal;
loader.register_task(Stage.JAVASCRIPT_INITIALIZING, {
name: "setup",
priority: 110,
function: async () => {
await import("tc-shared/proto");
await i18n.initialize();
ipc.setupIpcHandler();
setupJSRender();
}
});
loader.register_task(Stage.JAVASCRIPT_INITIALIZING, {
name: "main app connect",
priority: 100,
function: async () => {
const ppController = getPopoutController();
await ppController.initialize();
}
});
loader.register_task(Stage.JAVASCRIPT_INITIALIZING, {
name: "modal renderer loader",
priority: 10,
function: async () => {
modalRenderer = new ModalRenderer({
close() {
getPopoutController().doClose()
},
minimize() {
getPopoutController().doMinimize()
}
});
}
});
loader.register_task(Stage.JAVASCRIPT_INITIALIZING, {
name: "modal class loader",
priority: 10,
function: async () => {
const modalTarget = AppParameters.getValue(AppParameters.KEY_MODAL_TARGET, "unknown");
console.error("Loading modal class %s", modalTarget);
try {
const registeredModal = findRegisteredModal(modalTarget as any);
if(!registeredModal) {
loader.critical_error("Missing popout handler", "Handler " + modalTarget + " is missing.");
throw "missing handler";
}
modalClass = (await registeredModal.classLoader()).default;
} catch(error) {
loader.critical_error("Failed to load modal", "Lookup the console for more detail");
console.error("Failed to load modal %s: %o", modalTarget, error);
}
}
});
loader.register_task(Stage.LOADED, {
name: "main app connect",
priority: 100,
function: async () => {
try {
modalInstance = constructAbstractModalClass(modalClass, { windowed: true }, getPopoutController().getConstructorArguments());
modalRenderer.renderModal(modalInstance);
} catch(error) {
loader.critical_error("Failed to invoker modal", "Lookup the console for more detail");
console.error("Failed to load modal: %o", error);
}
}
});

View File

@ -1,17 +0,0 @@
import {ModalInstanceController, ModalOptions} from "../modal/Definitions";
import "./Controller";
export type ControllerFactory = (modalType: string, constructorArguments?: any[], options?: ModalOptions) => ModalInstanceController;
let modalControllerFactory: ControllerFactory;
export function setExternalModalControllerFactory(factory: ControllerFactory) {
modalControllerFactory = factory;
}
export function spawnExternalModal<EventClass extends { [key: string]: any }>(modalType: string, constructorArguments: any[], options: ModalOptions) : ModalInstanceController {
if(typeof modalControllerFactory === "undefined") {
throw tr("No external modal factory has been set");
}
return modalControllerFactory(modalType, constructorArguments, options);
}

View File

@ -10,9 +10,11 @@ import {
} from "tc-shared/ui/react-elements/modal/Definitions";
import {Registry} from "tc-events";
import {findRegisteredModal, RegisteredModal} from "tc-shared/ui/react-elements/modal/Registry";
import {spawnExternalModal} from "tc-shared/ui/react-elements/external-modal";
import {InternalModalInstance} from "tc-shared/ui/react-elements/modal/internal";
import {assertMainApplication} from "tc-shared/ui/utils";
import {InternalModalInstance} from "./internal";
import {ExternalModalController} from "./external/Controller";
assertMainApplication();
export class GenericModalController<T extends keyof ModalConstructorArguments> implements ModalController {
private readonly events: Registry<ModalEvents>;
@ -66,7 +68,7 @@ export class GenericModalController<T extends keyof ModalConstructorArguments> i
private createModalInstance() {
if(this.popedOut) {
this.instance = spawnExternalModal(this.modalType, this.modalConstructorArguments, this.modalOptions);
this.instance = new ExternalModalController(this.modalType, this.modalConstructorArguments, this.modalOptions);
} else {
this.instance = new InternalModalInstance(this.getModalClass(), this.modalConstructorArguments, this.modalOptions);
}
@ -74,6 +76,11 @@ export class GenericModalController<T extends keyof ModalConstructorArguments> i
const events = this.instance.getEvents();
events.on("notify_destroy", events.on("notify_open", () => this.events.fire("open")));
events.on("notify_destroy", events.on("notify_close", () => this.events.fire("close")));
events.on("notify_destroy", () => {
if(this.instance) {
this.destroy();
}
});
events.on("action_close", () => this.destroy());
events.on("action_popout", () => {
@ -99,8 +106,9 @@ export class GenericModalController<T extends keyof ModalConstructorArguments> i
}
private destroyModalInstance() {
this.instance?.destroy();
const instance = this.instance;
this.instance = undefined;
instance?.destroy();
}
destroy() {

View File

@ -69,10 +69,12 @@ export enum ModalState {
}
export interface ModalInstanceEvents {
/* Actions which must be implemented by our modal owner */
action_close: {},
action_minimize: {},
action_popout: {},
/* State changes we encountered */
notify_open: {}
notify_minimize: {},
notify_close: {},

View File

@ -135,6 +135,27 @@ html:root {
display: flex;
flex-direction: column;
justify-content: stretch;
&.windowed {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
width: 100%;
height: 100%;
.modalTitle {
-webkit-app-region: drag;
}
.modalBody {
height: 100%;
width: 100%;
}
}
}
.modalWindowContainer {

View File

@ -123,11 +123,12 @@ export class ModalBodyRenderer extends React.PureComponent<{
}
export class ModalFrameRenderer extends React.PureComponent<{
windowed: boolean,
children: [React.ReactElement<ModalFrameTopRenderer>, React.ReactElement<ModalBodyRenderer>]
}> {
render() {
return (
<div className={cssStyle.modalFrame}>
<div className={cssStyle.modalFrame + " " + (this.props.windowed ? cssStyle.windowed : "")}>
{this.props.children[0]}
{this.props.children[1]}
</div>

View File

@ -0,0 +1,219 @@
import {LogCategory, logError, logInfo, logWarn} from "tc-shared/log";
import {getIpcInstance, IPCChannel} from "tc-shared/ipc/BrowserIPC";
import {Registry} from "tc-events";
import {ModalOptions} from "tc-shared/ui/react-elements/modal/Definitions";
import {guid} from "tc-shared/crypto/uid";
import {ModalInstanceController, ModalInstanceEvents, ModalState} from "tc-shared/ui/react-elements/modal/Definitions";
import {getWindowManager} from "tc-shared/ui/windows/WindowManager";
import {assertMainApplication} from "tc-shared/ui/utils";
import {
ModalIPCController2Renderer,
ModalIPCRenderer2ControllerMessages
} from "tc-shared/ui/react-elements/modal/external/Definitions";
assertMainApplication();
export class ExternalModalController implements ModalInstanceController {
private readonly modalType: string;
private readonly modalOptions: ModalOptions;
private readonly constructorArguments: any[];
private readonly mainModalId: string;
private readonly ipcMessageHandler: { [key: string]: (sourcePeerId: string, message: any) => void };
private ipcRemotePeerId: string;
private ipcChannel: IPCChannel;
private readonly modalEvents: Registry<ModalInstanceEvents>;
private modalInitializeCallback: () => void;
private windowId: string | undefined;
private windowListener: (() => void)[];
private windowMutatePromise: Promise<void>;
constructor(modalType: string, constructorArguments: any[], modalOptions: ModalOptions) {
this.modalType = modalType;
this.modalOptions = modalOptions;
this.constructorArguments = constructorArguments;
this.mainModalId = guid();
this.modalEvents = new Registry<ModalInstanceEvents>();
this.ipcMessageHandler = {};
this.ipcChannel = getIpcInstance().createChannel(guid());
this.ipcChannel.messageHandler = (sourcePeerId, broadcast, message) => {
if(sourcePeerId !== this.ipcRemotePeerId && message.type !== "hello-renderer") {
return;
}
if(typeof this.ipcMessageHandler[message.type] !== "function") {
logWarn(LogCategory.IPC, tr("Received remote modal message but we don't know how to handle the message (%s)."), message.type);
return;
}
this.ipcMessageHandler[message.type](sourcePeerId, message.data);
};
this.registerIpcMessageHandler("hello-renderer", (sourcePeerId, message) => {
if(this.ipcRemotePeerId) {
logInfo(LogCategory.IPC, tr("Modal %s got reloaded (Version: %s). Using new peer address %s instead of %s and initializing it."), this.modalType, message.version, this.ipcRemotePeerId, sourcePeerId);
this.sendIpcMessage("invalidate-modal-instance", { });
} else {
logInfo(LogCategory.IPC, tr("Modal %s has called back (Version: %s). Initializing it."), this.modalType, message.version);
}
this.ipcRemotePeerId = sourcePeerId;
if(message.version !== __build.version) {
this.sendIpcMessage("hello-controller", { accepted: false, message: tr("version miss match") });
return;
}
if(this.modalInitializeCallback) {
this.modalInitializeCallback();
}
this.sendIpcMessage("hello-controller", {
accepted: true,
modalId: this.mainModalId,
modalType: this.modalType,
constructorArguments: this.constructorArguments
});
});
this.registerIpcMessageHandler("invoke-modal-action", (sourcePeerId, message) => {
if(message.modalId !== this.mainModalId) {
return;
}
switch (message.action) {
case "minimize":
this.modalEvents.fire("action_minimize");
break;
case "close":
this.modalEvents.fire("action_close");
break;
}
});
}
destroy() {
this.hide().then(undefined);
this.modalEvents.destroy();
}
getEvents(): Registry<ModalInstanceEvents> {
return this.modalEvents;
}
getState(): ModalState {
return typeof this.windowId === "string" ? ModalState.SHOWN : ModalState.DESTROYED;
}
async show(): Promise<void> {
await this.mutateWindow(async () => {
const windowManager = getWindowManager();
if(typeof this.windowId === "string") {
if(windowManager.isActionSupported(this.windowId, "focus")) {
await windowManager.executeAction(this.windowId, "focus");
}
return;
}
const result = await windowManager.createWindow({
uniqueId: this.modalOptions.uniqueId || this.modalType,
loaderTarget: "modal-external",
windowName: "modal " + this.modalType,
defaultSize: this.modalOptions.defaultSize,
appParameters: {
"modal-channel": this.ipcChannel.channelId,
}
});
if(result.status === "error-user-rejected") {
throw tr("user rejected");
} else if(result.status === "error-unknown") {
throw result.message;
}
this.windowListener = [];
this.windowListener.push(windowManager.getEvents().on("notify_window_destroyed", event => {
if(event.windowId !== this.windowId) {
return;
}
this.handleWindowDestroyed();
}));
this.windowId = result.windowId;
try {
await new Promise((resolve, reject) => {
this.modalInitializeCallback = resolve;
setTimeout(reject, 15000);
});
} catch (_) {
logError(LogCategory.IPC, tr("Opened modal failed to call back within 15 seconds."));
getWindowManager().destroyWindow(this.windowId);
} finally {
this.modalInitializeCallback = undefined;
}
});
}
async hide(): Promise<void> {
await this.mutateWindow(async () => {
if(typeof this.windowId !== "string") {
return;
}
getWindowManager().destroyWindow(this.windowId);
});
}
private async mutateWindow<T>(callback: () => Promise<T>) : Promise<T> {
while(this.windowMutatePromise) {
await this.windowMutatePromise;
}
return await new Promise((resolveOuter, rejectOuter) => {
const promise = callback();
this.windowMutatePromise = new Promise<void>(resolve => {
promise.then(result => {
this.windowMutatePromise = undefined;
resolve();
resolveOuter(result);
}).catch(error => {
this.windowMutatePromise = undefined;
resolve();
rejectOuter(error);
});
});
});
}
private handleWindowDestroyed() {
this.windowId = undefined;
this.windowListener.forEach(callback => callback());
this.modalEvents.fire("notify_destroy");
}
private registerIpcMessageHandler<T extends keyof ModalIPCRenderer2ControllerMessages>(
type: T,
handler: (sourcePeerId: string, message: ModalIPCRenderer2ControllerMessages[T]) => void
) {
this.ipcMessageHandler[type] = handler;
}
private sendIpcMessage<T extends keyof ModalIPCController2Renderer>(type: T, message: ModalIPCController2Renderer[T]) {
if(!this.ipcRemotePeerId) {
logWarn(LogCategory.IPC, tr("Tried to send a modal message but we don't know the modals peer address."));
return;
}
this.ipcChannel.sendMessage(type, message, this.ipcRemotePeerId);
}
}

View File

@ -0,0 +1,38 @@
export interface ModalIPCMessages {
"hello-renderer": { version: string },
"hello-controller": {
accepted: true,
modalId: string,
modalType: string,
constructorArguments: any[],
} | {
accepted: false,
message: string
},
/*
"create-inline-modal": {
modalId: string,
modalType: string,
constructorArguments: any[],
},
"destroy-inline-modal": {
},
*/
"invoke-modal-action": {
modalId: string,
action: "close" | "minimize"
},
/* The controller has a new peer which authenticated for the modal */
"invalidate-modal-instance": {}
}
export type ModalIPCRenderer2ControllerMessages = Pick<ModalIPCMessages, "hello-renderer" | "invoke-modal-action">;
export type ModalIPCController2Renderer = Pick<ModalIPCMessages, "hello-controller" | "invalidate-modal-instance">;
export type ModalIPCMessage<Messages, T extends keyof Messages = keyof Messages> = {
type: T,
payload: Messages[T]
}

View File

@ -0,0 +1,106 @@
import {getIpcInstance, IPCChannel} from "tc-shared/ipc/BrowserIPC";
import {
ModalIPCController2Renderer,
ModalIPCRenderer2ControllerMessages
} from "../Definitions";
import {LogCategory, logDebug, logWarn} from "tc-shared/log";
export type ModalInstanceInitializeResult = {
status: "success",
modalId: string,
modalType: string,
constructorArguments: any[],
} | {
status: "timeout"
} | {
status: "rejected",
message: string
};
export class ModalWindowControllerInstance {
private readonly ipcMessageHandler: { [key: string]: (sourcePeerId: string, message: any) => void };
private ipcRemotePeerId: string;
private ipcChannel: IPCChannel;
constructor(modalChannelId: string) {
this.ipcMessageHandler = {};
this.ipcChannel = getIpcInstance().createCoreControlChannel(modalChannelId);
this.ipcChannel.messageHandler = (sourcePeerId, broadcast, message) => {
if(this.ipcRemotePeerId && sourcePeerId !== this.ipcRemotePeerId) {
return;
}
if(typeof this.ipcMessageHandler[message.type] !== "function") {
logWarn(LogCategory.IPC, tr("Received remote controller message but we don't know how to handle the message (%s)."), message.type);
return;
}
this.ipcMessageHandler[message.type](sourcePeerId, message.data);
};
this.registerIpcMessageHandler("invalidate-modal-instance", () => {
logWarn(LogCategory.IPC, tr("This modal instance has been invalidated."));
/* TODO: Show so on the screen. */
});
}
async initialize() : Promise<ModalInstanceInitializeResult> {
this.sendIpcMessage("hello-renderer", {
version: __build.version
});
let controllerResult: ModalIPCController2Renderer["hello-controller"];
try {
controllerResult = await new Promise((resolve, reject) => {
this.registerIpcMessageHandler("hello-controller", (sourcePeerId, message) => {
logDebug(LogCategory.IPC, tr("Found remote controller peer id: %s"), sourcePeerId);
this.ipcRemotePeerId = sourcePeerId;
resolve(message);
});
setTimeout(reject, 15000);
});
} catch (_) {
return { status: "timeout" };
}
if(controllerResult.accepted === true) {
return {
status: "success",
modalId: controllerResult.modalId,
modalType: controllerResult.modalType,
constructorArguments: controllerResult.constructorArguments
};
} else {
return {
status: "rejected",
message: controllerResult.message
};
}
}
triggerModalAction(modalId: string, action: "minimize" | "close") {
this.sendIpcMessage("invoke-modal-action", {
modalId: modalId,
action: action
});
}
private registerIpcMessageHandler<T extends keyof ModalIPCController2Renderer>(
type: T,
handler: (sourcePeerId: string, message: ModalIPCController2Renderer[T]) => void
) {
this.ipcMessageHandler[type] = handler;
}
private sendIpcMessage<T extends keyof ModalIPCRenderer2ControllerMessages>(type: T, message: ModalIPCRenderer2ControllerMessages[T]) {
if(!this.ipcRemotePeerId && type !== "hello-renderer") {
logWarn(LogCategory.IPC, tr("Tried to send a controller message but we don't know the controllers peer address."));
return;
}
this.ipcChannel.sendMessage(type, message, this.ipcRemotePeerId);
}
}

View File

@ -0,0 +1,93 @@
import * as loader from "tc-loader";
import {setupIpcHandler} from "tc-shared/ipc/BrowserIPC";
import {initializeI18N} from "tc-shared/i18n/localize";
import {Stage} from "tc-loader";
import {AbstractModal, constructAbstractModalClass} from "tc-shared/ui/react-elements/modal/Definitions";
import {AppParameters} from "tc-shared/settings";
import {setupJSRender} from "tc-shared/ui/jsrender";
import {findRegisteredModal} from "tc-shared/ui/react-elements/modal/Registry";
import {ModalWindowControllerInstance} from "./Controller";
import {LogCategory, logError, logInfo} from "tc-shared/log";
import {ModalRenderer} from "./ModalRenderer";
import "../../../../../file/RemoteAvatars";
import "../../../../../file/RemoteIcons";
let instanceController: ModalWindowControllerInstance;
let mainModalId: string;
let mainModalRenderer: ModalRenderer;
let mainModalInstance: AbstractModal;
loader.register_task(Stage.JAVASCRIPT_INITIALIZING, {
name: "setup",
priority: 110,
function: async () => {
await import("tc-shared/proto");
await initializeI18N();
setupIpcHandler();
setupJSRender();
}
});
loader.register_task(Stage.JAVASCRIPT_INITIALIZING, {
name: "modal renderer initialize",
priority: 100,
function: async () => {
mainModalRenderer = new ModalRenderer({
close() {
instanceController?.triggerModalAction(mainModalId, "close");
},
minimize() {
instanceController?.triggerModalAction(mainModalId, "minimize");
}
});
}
});
loader.register_task(Stage.JAVASCRIPT_INITIALIZING, {
name: "modal initialize",
priority: 10,
function: initializeModalRenderer
});
async function initializeModalRenderer(taskId) {
loader.setCurrentTaskName(taskId, tr("connecting to controller"));
instanceController = new ModalWindowControllerInstance(AppParameters.getValue(AppParameters.KEY_MODAL_IPC_CHANNEL, "invalid"));
const result = await instanceController.initialize();
if(result.status === "timeout") {
loader.critical_error("Modal controller timeout", "Modal controller failed to call back.");
throw "modal controller timeout";
} else if(result.status === "rejected") {
loader.critical_error("Modal controller reject", result.message || tr("unknown why"));
throw "modal controller reject";
}
mainModalId = result.modalId;
loader.setCurrentTaskName(taskId, tr("loading modal class"));
let modalClass: new (...args: any[]) => AbstractModal;
logInfo(LogCategory.GENERAL, tr("Loading modal class %s"), result.modalType);
try {
const registeredModal = findRegisteredModal(result.modalType as any);
if(!registeredModal) {
loader.critical_error(tr("Unknown modal"), tra("Modal {} is unknown", result.modalType));
throw "missing modal";
}
modalClass = (await registeredModal.classLoader()).default;
} catch(error) {
loader.critical_error("Failed to load modal", "Lookup the console for more detail");
logError(LogCategory.GENERAL,tr("Failed to load main modal %s: %o"), result.modalType, error);
}
loader.setCurrentTaskName(taskId, tr("initializing modal class"));
try {
mainModalInstance = constructAbstractModalClass(modalClass, { windowed: true }, result.constructorArguments);
mainModalRenderer.renderModal(mainModalInstance);
} catch(error) {
loader.critical_error("Failed to invoker modal", "Lookup the console for more detail");
logError(LogCategory.GENERAL,tr("Failed to load modal: %o"), error);
}
}

View File

@ -1,8 +1,8 @@
@import "../../../../css/static/mixin";
@import "../../../../../../css/static/mixin";
/* FIXME: Remove this wired import */
@import "../../../../css/static/general";
@import "../../../../css/static/modal";
@import "../../../../../../css/static/general";
@import "../../../../../../css/static/modal";
html, body {
margin: 0;
@ -15,4 +15,4 @@ html, body {
color: #999;
overflow: hidden;
}
}

View File

@ -33,7 +33,7 @@ export class ModalRenderer {
if(__build.target === "client") {
ReactDOM.render(
<ModalFrameRenderer>
<ModalFrameRenderer windowed={true}>
<ModalFrameTopRenderer
replacePageTitle={true}
modalInstance={modal}

View File

@ -66,7 +66,7 @@ export class InternalModalInstance implements ModalInstanceController {
await new Promise(resolve => {
ReactDOM.render(
<PageModalRenderer modalInstance={this.modalInstance} onBackdropClicked={this.getCloseCallback()} ref={this.rendererInstance}>
<ModalFrameRenderer>
<ModalFrameRenderer windowed={false}>
<ModalFrameTopRenderer
replacePageTitle={false}
modalInstance={this.modalInstance}

View File

@ -0,0 +1,50 @@
import {Registry} from "tc-events";
export type WindowCreateResult = {
status: "success",
windowId: string
} | {
status: "error-unknown",
message: string
} | {
status: "error-user-rejected",
};
export interface WindowManagerEvents {
notify_window_created: { windowId: string },
notify_window_focused: { windowId: string },
notify_window_destroyed: { windowId: string },
}
export type WindowAction = "focus" | "maximize" | "minimize";
export interface WindowSpawnOptions {
uniqueId: string,
loaderTarget: string,
/* if the window hasn't been created within a user gesture the client will be prompted if he wants to open the window */
windowName?: string,
appParameters?: {[key: string]: string},
defaultSize?: { width: number, height: number },
}
export interface WindowManager {
getEvents() : Registry<WindowManagerEvents>;
createWindow(options: WindowSpawnOptions) : Promise<WindowCreateResult>;
destroyWindow(windowId: string);
getActiveWindowId() : string | undefined;
isActionSupported(windowId: string, action: WindowAction) : boolean;
executeAction(windowId: string, action: WindowAction) : Promise<void>;
}
let windowManager: WindowManager;
export function getWindowManager() : WindowManager {
return windowManager;
}
export function setWindowManager(newWindowManager: WindowManager) {
windowManager = newWindowManager;
}

View File

@ -1,155 +0,0 @@
import {AbstractExternalModalController} from "tc-shared/ui/react-elements/external-modal/Controller";
import {spawnYesNo} from "tc-shared/ui/modal/ModalYesNo";
import {ChannelMessage, getIpcInstance} from "tc-shared/ipc/BrowserIPC";
import {LogCategory, logDebug, logWarn} from "tc-shared/log";
import {Popout2ControllerMessages, PopoutIPCMessage} from "tc-shared/ui/react-elements/external-modal/IPCMessage";
import {tr, tra} from "tc-shared/i18n/localize";
import {ModalOptions} from "tc-shared/ui/react-elements/modal/Definitions";
import {assertMainApplication} from "tc-shared/ui/utils";
assertMainApplication();
export class ExternalModalController extends AbstractExternalModalController {
private readonly options: ModalOptions;
private currentWindow: Window;
private windowClosedTestInterval: number = 0;
private windowClosedTimeout: number;
constructor(modalType: string, constructorArguments: any[] | undefined, options: ModalOptions | undefined) {
super(modalType, constructorArguments);
this.options = options || {};
}
protected async spawnWindow() : Promise<boolean> {
if(this.currentWindow) {
return true;
}
this.currentWindow = this.trySpawnWindow0();
if(!this.currentWindow) {
await new Promise((resolve, reject) => {
spawnYesNo(tr("Would you like to open the popup?"), tra("Would you like to open popup {}?", this.modalType), callback => {
if(!callback) {
reject("user aborted");
return;
}
this.currentWindow = this.trySpawnWindow0();
if(window) {
reject(tr("Failed to spawn window"));
} else {
resolve();
}
}).close_listener.push(() => reject(tr("user aborted")));
});
}
if(!this.currentWindow) {
return false;
}
this.currentWindow.onbeforeunload = () => {
clearInterval(this.windowClosedTestInterval);
this.windowClosedTimeout = Date.now() + 5000;
this.windowClosedTestInterval = setInterval(() => {
if(!this.currentWindow) {
clearInterval(this.windowClosedTestInterval);
this.windowClosedTestInterval = 0;
return;
}
if(this.currentWindow.closed || Date.now() > this.windowClosedTimeout) {
clearInterval(this.windowClosedTestInterval);
this.windowClosedTestInterval = 0;
this.handleWindowClosed();
}
}, 100);
};
return true;
}
protected destroyWindow() {
clearInterval(this.windowClosedTestInterval);
this.windowClosedTestInterval = 0;
if(this.currentWindow) {
this.currentWindow.close();
this.currentWindow = undefined;
}
}
protected focusWindow() {
this.currentWindow?.focus();
}
private trySpawnWindow0() : Window | null {
const parameters = {
"loader-target": "manifest",
"chunk": "modal-external",
"modal-target": this.modalType,
"modal-identify": this.ipcAuthenticationCode,
"ipc-address": getIpcInstance().getApplicationChannelId(),
"ipc-core-peer": getIpcInstance().getLocalPeerId(),
"disableGlobalContextMenu": __build.mode === "debug" ? 1 : 0,
"loader-abort": __build.mode === "debug" ? 1 : 0,
};
const options = this.getOptions();
const features = {
status: "no",
location: "no",
toolbar: "no",
menubar: "no",
resizable: "yes",
width: options.defaultSize?.width,
height: options.defaultSize?.height
};
let baseUrl = location.origin + location.pathname + "?";
return window.open(
baseUrl + Object.keys(parameters).map(e => e + "=" + encodeURIComponent(parameters[e])).join("&"),
this.options?.uniqueId || this.modalType,
Object.keys(features).map(e => e + "=" + features[e]).join(",")
);
}
protected handleIPCMessage(remoteId: string, broadcast: boolean, message: ChannelMessage) {
if(!broadcast && this.ipcRemotePeerId !== remoteId) {
if(this.windowClosedTestInterval > 0) {
clearInterval(this.windowClosedTestInterval);
this.windowClosedTestInterval = 0;
logDebug(LogCategory.IPC, tr("Remote window got reconnected. Client reloaded it."));
} else {
logWarn(LogCategory.IPC, tr("Remote window got a new id. Maybe a reload?"));
}
}
super.handleIPCMessage(remoteId, broadcast, message);
}
protected handleTypedIPCMessage<T extends Popout2ControllerMessages>(remoteId: string, isBroadcast: boolean, type: T, payload: PopoutIPCMessage[T]) {
super.handleTypedIPCMessage(remoteId, isBroadcast, type, payload);
if(isBroadcast) {
return;
}
switch (type) {
case "invoke-modal-action":
const data = payload as PopoutIPCMessage["invoke-modal-action"];
switch (data.action) {
case "close":
this.destroy();
break;
case "minimize":
window.focus();
break;
}
break;
}
}
}

View File

@ -7,7 +7,9 @@ window.addEventListener("beforeunload", event => {
}
const active_connections = server_connections.getAllConnectionHandlers().filter(e => e.connected);
if(active_connections.length == 0) return;
if(active_connections.length == 0) {
return;
}
event.returnValue = "Are you really sure?<br>You're still connected!";
});

197
web/app/WebWindowManager.ts Normal file
View File

@ -0,0 +1,197 @@
import {
WindowAction,
WindowCreateResult,
WindowManager,
WindowManagerEvents,
WindowSpawnOptions
} from "tc-shared/ui/windows/WindowManager";
import {assertMainApplication} from "tc-shared/ui/utils";
import {Registry} from "tc-events";
import {getIpcInstance} from "tc-shared/ipc/BrowserIPC";
import _ from "lodash";
import {spawnYesNo} from "tc-shared/ui/modal/ModalYesNo";
import {tr, tra} from "tc-shared/i18n/localize";
import {guid} from "tc-shared/crypto/uid";
assertMainApplication();
type WindowHandle = {
window: Window,
uniqueId: string,
destroy: () => void,
closeTestInterval: number | undefined,
closeTestTimeout: number | undefined,
}
export class WebWindowManager implements WindowManager {
private readonly events: Registry<WindowManagerEvents>;
private registeredWindows: { [key: string]: WindowHandle } = {};
constructor() {
this.events = new Registry<WindowManagerEvents>();
/* TODO: Close all active windows on page unload */
}
getEvents(): Registry<WindowManagerEvents> {
return this.events;
}
async createWindow(options: WindowSpawnOptions): Promise<WindowCreateResult> {
/* Multiple application instance may want to open the same windows */
const windowUniqueId = getIpcInstance().getApplicationChannelId() + "-" + options.uniqueId;
/* If we're opening a window with the same unique id we need to destroy the old handle */
for(const windowId of Object.keys(this.registeredWindows)) {
if(this.registeredWindows[windowId].uniqueId === windowUniqueId) {
this.registeredWindows[windowId].destroy();
}
}
let windowInstance = this.tryCreateWindow(options, windowUniqueId);
if(!windowInstance) {
try {
await new Promise((resolve, reject) => {
spawnYesNo(tr("Would you like to open the popup?"), tra("Would you like to open window {}?", options.windowName), callback => {
if(!callback) {
reject();
return;
}
windowInstance = this.tryCreateWindow(options, windowUniqueId);
resolve();
});
});
} catch (_) {
return { status: "error-user-rejected" };
}
}
if(!windowInstance) {
return { status: "error-user-rejected" };
}
const windowId = guid();
const windowHandle = this.registeredWindows[windowId] = {
window: windowInstance,
uniqueId: windowUniqueId,
closeTestInterval: 0,
closeTestTimeout: 0,
destroy: undefined
};
const handleWindowClosed = () => {
if(windowHandle.window && !windowHandle.window.closed) {
windowHandle.window.onbeforeunload = undefined;
windowHandle.window.onunload = undefined;
windowHandle.window.onclose = undefined;
windowHandle.window.close();
}
clearInterval(windowHandle.closeTestInterval);
clearTimeout(windowHandle.closeTestTimeout);
if(!this.registeredWindows[windowId]) {
return;
}
delete this.registeredWindows[windowId];
this.events.fire("notify_window_destroyed", { windowId: windowId });
};
const testWindowClosed = (timeout: number) => {
clearInterval(windowHandle.closeTestInterval);
clearTimeout(windowHandle.closeTestTimeout);
windowHandle.closeTestInterval = setInterval(() => {
/* !== is required for compatibility with Opera */
if(windowHandle.window.closed !== false) {
handleWindowClosed();
}
}, 100);
windowHandle.closeTestTimeout = setTimeout(() => {
clearInterval(windowHandle.closeTestInterval);
clearTimeout(windowHandle.closeTestTimeout);
}, timeout);
};
windowInstance.onbeforeunload = () => testWindowClosed(5000);
windowInstance.onunload = () => testWindowClosed(2500);
windowInstance.onclose = () => testWindowClosed(2500);
windowHandle.destroy = () => handleWindowClosed();
return { status: "success", windowId: windowId };
}
private tryCreateWindow(options: WindowSpawnOptions, uniqueId: string) : Window | null {
const parameters = _.cloneDeep(options.appParameters || {});
Object.assign(parameters, {
"loader-target": "manifest",
"loader-chunk": options.loaderTarget,
"loader-abort": __build.mode === "debug" ? 1 : 0,
"ipc-address": getIpcInstance().getApplicationChannelId(),
"ipc-core-peer": getIpcInstance().getLocalPeerId(),
"disableGlobalContextMenu": __build.mode === "debug" ? 1 : 0,
});
const features = {
status: "no",
location: "no",
toolbar: "no",
menubar: "no",
resizable: "yes",
width: options.defaultSize?.width,
height: options.defaultSize?.height
};
let baseUrl = location.origin + location.pathname + "?";
return window.open(
baseUrl + Object.keys(parameters).map(e => e + "=" + encodeURIComponent(parameters[e])).join("&"),
uniqueId,
Object.keys(features).map(e => e + "=" + features[e]).join(",")
);
}
destroyWindow(windowId: string) {
this.registeredWindows[windowId]?.destroy();
}
getActiveWindowId(): string | undefined {
/* TODO! */
return undefined;
}
async executeAction(windowId: string, action: WindowAction): Promise<void> {
const windowHandle = this.registeredWindows[windowId];
if(!windowHandle || windowHandle.window.closed) {
return;
}
switch (action) {
case "focus":
window.window.focus();
break;
case "minimize":
case "maximize":
/* we can't do so */
break;
}
}
isActionSupported(windowId: string, action: WindowAction) {
switch (action) {
case "focus":
return true;
case "maximize":
case "minimize":
default:
return false;
}
}
}

View File

@ -0,0 +1,14 @@
import "webrtc-adapter";
import "webcrypto-liner";
import "../index.scss";
import "../FileTransfer";
import "../hooks"
import "../UnloadHandler";
import "../ui/context-menu";
import "../ui/FaviconRenderer";
import "tc-shared/entry-points/MainApp";

View File

@ -0,0 +1,4 @@
/* This is the entry point file for the external modals */
import "../ui/context-menu";
import "tc-shared/entry-points/ModalWindow";

View File

@ -1,12 +0,0 @@
import * as loader from "tc-loader";
import {Stage} from "tc-loader";
import {setExternalModalControllerFactory} from "tc-shared/ui/react-elements/external-modal";
import {ExternalModalController} from "../ExternalModalFactory";
loader.register_task(Stage.JAVASCRIPT_INITIALIZING, {
priority: 50,
name: "external modal controller factory setup",
function: async () => {
setExternalModalControllerFactory((modalType, constructorArguments, options) => new ExternalModalController(modalType, constructorArguments, options));
}
});

View File

@ -0,0 +1,10 @@
import * as loader from "tc-loader";
import {Stage} from "tc-loader";
import {setWindowManager} from "tc-shared/ui/windows/WindowManager";
import {WebWindowManager} from "../WebWindowManager";
loader.register_task(Stage.JAVASCRIPT_INITIALIZING, {
name: "window manager init",
function: async () => setWindowManager(new WebWindowManager()),
priority: 100
});

View File

@ -1,9 +1,10 @@
import "./AudioBackend";
import "./AudioRecorder";
import "./Backend";
import "./Dns";
import "./DNS";
import "./MenuBar";
import "./ServerConnection";
import "./Sounds";
import "./Video";
import "./KeyBoard";
import "./KeyBoard";
import "./WindowManager";

View File

@ -1,4 +0,0 @@
/* This is the entry point file for the external modals */
import "./ui/context-menu";
import "tc-shared/ui/react-elements/external-modal/PopoutEntrypoint"

View File

@ -1,14 +0,0 @@
import "webrtc-adapter";
import "webcrypto-liner";
import "./index.scss";
import "./FileTransfer";
import "./hooks"
import "./UnloadHandler";
import "./ui/context-menu";
import "./ui/FaviconRenderer";
export = require("tc-shared/main");

View File

@ -3,16 +3,17 @@ import * as config_base from "./webpack.config";
export = env => config_base.config(env, "client").then(config => {
Object.assign(config.entry, {
"client-app": ["./client/app/index.ts"],
"modal-external": ["./client/app/EntryPointPopoutModal.ts"]
"shared-app": ["./client/app/entry-points/AppMain.ts"],
"modal-external": ["./client/app/entry-points/ModalWindow.ts"]
});
Object.assign(config.resolve.alias, {
"tc-shared": path.resolve(__dirname, "shared/js"),
});
if(!Array.isArray(config.externals))
if(!Array.isArray(config.externals)) {
throw "invalid config";
}
config.externals.push(({ context, request }, callback) => {
if (request.startsWith("tc-backend/")) {

View File

@ -3,8 +3,8 @@ import * as config_base from "./webpack.config";
export = env => config_base.config(env, "web").then(config => {
Object.assign(config.entry, {
"shared-app": ["./web/app/index.ts"],
"modal-external": ["./web/app/index-external.ts"]
"shared-app": ["./web/app/entry-points/AppMain.ts"],
"modal-external": ["./web/app/entry-points/ModalWindow.ts"]
});
Object.assign(config.resolve.alias, {

View File

@ -118,8 +118,7 @@ export const config = async (env: any, target: "web" | "client"): Promise<Config
return {
entry: {
"loader": "./loader/app/index.ts",
"modal-external": "./shared/js/ui/react-elements/external-modal/PopoutEntrypoint.ts",
//"devel-main": "./shared/js/devel_main.ts"
"modal-external": "./shared/js/entry-points/ModalWindow.ts",
},
devtool: isDevelopment ? "inline-source-map" : "source-map",

View File

@ -62,6 +62,17 @@ class ManifestGenerator {
}
for(const module of compilation.chunkGraph.getChunkModules(chunk)) {
const moduleId = compilation.chunkGraph.getModuleId(module);
if(typeof moduleId === "string" && moduleId.startsWith("svg-sprites/")) {
/* custom svg sprite handler */
modules.push({
id: module.id,
context: "svg-sprites",
resource: moduleId.substring("svg-sprites/".length)
});
continue;
}
if(!module.type.startsWith("javascript/")) {
continue;
}
@ -70,16 +81,6 @@ class ManifestGenerator {
continue;
}
if(module.context.startsWith("svg-sprites/")) {
/* custom svg sprite handler */
modules.push({
id: module.id,
context: "svg-sprites",
resource: module.context.substring("svg-sprites/".length)
});
continue;
}
if(!(module instanceof NormalModule)) {
continue;
}
@ -94,7 +95,7 @@ class ManifestGenerator {
}
modules.push({
id: compilation.chunkGraph.getModuleId(module),
id: moduleId,
context: path.relative(this.options.context, module.context).replace(/\\/g, "/"),
resource: path.basename(module.resource)
});