diff --git a/shared/js/file/LocalIcons.ts b/shared/js/file/LocalIcons.ts index 67864698..41173e62 100644 --- a/shared/js/file/LocalIcons.ts +++ b/shared/js/file/LocalIcons.ts @@ -12,7 +12,6 @@ import {CommandResult} from "tc-shared/connection/ServerConnectionDeclaration"; import {ErrorCode} from "tc-shared/connection/ErrorCode"; import {ChannelMessage, IPCChannel} from "tc-shared/ipc/BrowserIPC"; import * as ipc from "tc-shared/ipc/BrowserIPC"; -import {kIPCAvatarChannel} from "tc-shared/file/Avatars"; /* TODO: Retry icon download after some time */ /* TODO: Download icon when we're connected to the server were we want the icon from and update the icon */ diff --git a/shared/js/ui/context-menu/Ipc.ts b/shared/js/ui/context-menu/Ipc.ts new file mode 100644 index 00000000..97031af0 --- /dev/null +++ b/shared/js/ui/context-menu/Ipc.ts @@ -0,0 +1,164 @@ +import * as loader from "tc-loader"; +import {Stage} from "tc-loader"; + +import { + ContextMenuEntry, + ContextMenuFactory, + setGlobalContextMenuFactory +} from "tc-shared/ui/context-menu/index"; +import {ChannelMessage, IPCChannel} from "tc-shared/ipc/BrowserIPC"; +import * as ipc from "tc-shared/ipc/BrowserIPC"; +import {Settings} from "tc-shared/settings"; +import {reactContextMenuInstance} from "tc-shared/ui/context-menu/ReactRenderer"; +import {getIconManager, RemoteIcon} from "tc-shared/file/Icons"; + +const kIPCContextMenuChannel = "context-menu"; + +class IPCContextMenu implements ContextMenuFactory { + private readonly ipcChannel: IPCChannel; + + private currentlyFocusedWindow: string; + private currentWindowFocused = true; + + private remoteContextMenuSupplierId: string; + + private uniqueEntryId: number = 0; + private menuCallbacks: {[key: string]: (() => void)} = {}; + private closeCallback: () => void; + + constructor() { + this.ipcChannel = ipc.getInstance().createChannel(Settings.instance.static(Settings.KEY_IPC_REMOTE_ADDRESS, undefined), kIPCContextMenuChannel); + this.ipcChannel.messageHandler = this.handleIpcMessage.bind(this); + + /* if we're just created we're the focused window ;) */ + this.currentWindowFocused = false; + this.handleWindowFocusReceived(); + + document.addEventListener("mousedown", () => this.handleWindowFocusReceived()); + } + + private handleWindowFocusReceived() { + if(!this.currentWindowFocused) { + if(this.closeCallback) { + this.closeCallback(); + } + this.closeCallback = undefined; + this.menuCallbacks = {}; + this.remoteContextMenuSupplierId = undefined; + + this.currentlyFocusedWindow = undefined; + this.currentWindowFocused = true; + this.ipcChannel.sendMessage("notify-focus-taken", {}); + } + } + + private wrapMenuEntryForRemote(entry: ContextMenuEntry) : ContextMenuEntry { + switch (entry.type) { + case "normal": + if(entry.subMenu) { + entry.subMenu = entry.subMenu.map(entry => this.wrapMenuEntryForRemote(entry)); + } + if(entry.icon instanceof RemoteIcon) { + entry.icon = { iconId: entry.icon.iconId, serverUniqueId: entry.icon.serverUniqueId } as any; + } + + /* fall through wanted! */ + case "checkbox": + if(!entry.click) { + return entry; + } + + if(!entry.uniqueId) { + entry.uniqueId = "r_" + (++this.uniqueEntryId); + } + + this.menuCallbacks[entry.uniqueId] = entry.click; + entry.click = undefined; + return entry; + + default: + return entry; + } + } + + private wrapMenuEntryFromRemote(entry: ContextMenuEntry) : ContextMenuEntry { + switch (entry.type) { + case "normal": + if(entry.subMenu) { + entry.subMenu = entry.subMenu.map(entry => this.wrapMenuEntryFromRemote(entry)); + } + if(entry.icon) { + const icon = entry.icon as any; + entry.icon = getIconManager().resolveIcon(icon.iconId, icon.serverUniqueId); + } + + /* fall through wanted! */ + case "checkbox": + if(!entry.uniqueId) { + return entry; + } + + entry.click = () => this.remoteContextMenuSupplierId && this.ipcChannel.sendMessage("notify-entry-click", { id: entry.uniqueId }, this.remoteContextMenuSupplierId); + return entry; + + default: + return entry; + } + } + + closeContextMenu() { + if(this.currentWindowFocused) { + reactContextMenuInstance.closeContextMenu(); + } + } + + spawnContextMenu(position: { pageX: number; pageY: number }, entries: ContextMenuEntry[], callbackClose?: () => void) { + if(this.currentWindowFocused) { + reactContextMenuInstance.spawnContextMenu(position, entries, callbackClose); + } else { + this.ipcChannel.sendMessage("spawn-context-menu", { + position: position, + entries: entries.map(entry => this.wrapMenuEntryForRemote(entry)) + }); + this.closeCallback = callbackClose; + } + } + + private handleIpcMessage(remoteId: string, _broadcast: boolean, message: ChannelMessage) { + if(message.type === "spawn-context-menu") { + if(!this.currentWindowFocused) { return; } + + reactContextMenuInstance.spawnContextMenu(message.data.position, message.data.entries.map(entry => this.wrapMenuEntryFromRemote(entry)), () => { + if(!this.remoteContextMenuSupplierId) { return; } + this.ipcChannel.sendMessage("notify-menu-close", {}, this.remoteContextMenuSupplierId); + + this.remoteContextMenuSupplierId = undefined; + }); + this.remoteContextMenuSupplierId = remoteId; + } else if(message.type === "notify-focus-taken") { + this.currentlyFocusedWindow = remoteId; + this.currentWindowFocused = false; + + /* close out context menu if we've any */ + reactContextMenuInstance.closeContextMenu(); + } else if(message.type === "notify-entry-click") { + const callback = this.menuCallbacks[message.data.id]; + if(!callback) { return; } + callback(); + } else if(message.type === "notify-menu-close") { + this.menuCallbacks = {}; + if(this.closeCallback) { + this.closeCallback(); + this.closeCallback = undefined; + } + } + } +} + +loader.register_task(Stage.JAVASCRIPT_INITIALIZING, { + priority: 80, + name: "context menu init", + function: async () => { + setGlobalContextMenuFactory(new IPCContextMenu()); + } +}) diff --git a/shared/js/ui/context-menu/ReactRenderer.scss b/shared/js/ui/context-menu/ReactRenderer.scss new file mode 100644 index 00000000..bc0c3467 --- /dev/null +++ b/shared/js/ui/context-menu/ReactRenderer.scss @@ -0,0 +1,186 @@ +@import "../../../css/static/mixin"; + +.globalContainer { + display: flex; + flex-direction: column; + + position: static; + + top: 0; + left: 0; + right: 0; + bottom: 0; +} + +.container { + overflow: visible; + display: block; + z-index: 120000; + position: absolute; + + opacity: 0; + + .menuContainer { + border: 1px solid #CCC; + white-space: nowrap; + font-family: sans-serif; + background: #FFF; + color: #333; + padding: 3px; + + &.left { + margin-left: -100%; + width: 100%; + } + + * { + font-family: Arial, serif; + font-size: 12px; + white-space: pre; + line-height: 1; + vertical-align: middle; + } + + .label { + /* nothing really to do here */ + + &.bold { + font-weight: bold; + } + } + + hr { + margin-top: 8px; + margin-bottom: 8px; + } + + .entry { + /*padding: 8px 12px;*/ + padding-right: 12px; + cursor: pointer; + list-style-type: none; + transition: all .3s ease; + user-select: none; + align-items: center; + + display: flex; + + &.disabled { + pointer-events: none; + background-color: lightgray; + cursor: not-allowed; + } + + &:hover:not(.disabled) { + background-color: #DEF; + } + } + + .icon { + margin-right: 4px; + } + + .arrow { + cursor: pointer; + pointer-events: all; + width: 7px; + height: 7px; + padding: 0; + margin-right: 5px; + margin-left: 5px; + + display: inline-block; + border: solid black; + border-width: 0 .2em .2em 0; + + transform: rotate(-45deg); + -webkit-transform: rotate(-45deg); + + position: absolute; + right: 3px; + } + + .subContainer { + margin-right: -3px; + padding-right: 24px; + position: relative; + + &:hover { + > .subMenu { + display: block; + } + } + } + + .subMenu { + display: none; + left: 100%; + top: -4px; + position: absolute; + margin-left: 0; + } + } + + &.shown { + pointer-events: all; + opacity: 1; + } +} + +.checkbox { + margin-top: 1px; + margin-left: 1px; + display: block; + position: relative; + padding-left: 14px; + margin-bottom: 12px; + cursor: pointer; + font-size: 22px; + + @include user-select(none); + + /* Hide the browser's default checkbox */ + input { + position: absolute; + opacity: 0; + cursor: pointer; + display: none; + } + + .checkmark { + position: absolute; + top: 0; + left: 0; + height: 11px; + width: 11px; + background-color: #eee; + + &:after { + content: ""; + position: absolute; + display: none; + + left: 4px; + top: 1px; + width: 3px; + height: 7px; + border: solid white; + border-width: 0 2px 2px 0; + -webkit-transform: rotate(45deg); + -ms-transform: rotate(45deg); + transform: rotate(45deg); + } + } + + &:hover input ~ .checkmark { + background-color: #ccc; + } + + input:checked ~ .checkmark { + background-color: #2196F3; + } + + input:checked ~ .checkmark:after { + display: block; + } +} \ No newline at end of file diff --git a/shared/js/ui/context-menu/ReactRenderer.tsx b/shared/js/ui/context-menu/ReactRenderer.tsx new file mode 100644 index 00000000..7ed0bce4 --- /dev/null +++ b/shared/js/ui/context-menu/ReactRenderer.tsx @@ -0,0 +1,273 @@ +import * as loader from "tc-loader"; +import {Stage} from "tc-loader"; +import { + closeContextMenu, + ContextMenuEntry, + ContextMenuEntryNormal, + ContextMenuFactory, MenuEntryLabel, +} from "tc-shared/ui/context-menu/index"; +import * as React from "react"; +import * as ReactDOM from "react-dom"; +import {IconRenderer, RemoteIconRenderer} from "tc-shared/ui/react-elements/Icon"; +import {useContext} from "react"; + +const cssStyle = require("./ReactRenderer.scss"); +const CloseCallback = React.createContext<() => void>(undefined); + +let globalMouseListener; +let globalContainer: HTMLDivElement; +let refRenderer = React.createRef(); + +const MenuEntryIconRenderer = (props: { entry: ContextMenuEntryNormal }) => { + if(!props.entry.icon || typeof props.entry.icon === "string") { + return ; + } else { + return ; + } +}; + +const MenuLabelRenderer = (props: { label: MenuEntryLabel }) => { + let text; + let classes = []; + if(typeof props.label === "string") { + text = props.label; + } else { + text = props.label.text; + if(props.label.bold) { + classes.push(cssStyle.bold); + } + } + + classes.push(cssStyle.label); + return
{text}
; +} + +const MenuEntryRenderer = (props: { entry: ContextMenuEntry }) => { + const closeCallback = useContext(CloseCallback); + const clickListener = () => { + closeCallback(); + + if("click" in props.entry && typeof props.entry.click === "function") { + props.entry.click(); + } + }; + + if(typeof props.entry.visible === "boolean" && !props.entry.visible) { return null; } + switch (props.entry.type) { + case "separator": + return
; + + case "checkbox": + return ( +
+ + +
+ ); + + case "normal": + return ( +
+ + + {!props.entry.subMenu?.length ? undefined : + +
+ + + } +
+ ); + } + return null; +} + +const MenuRenderer = (props: { entries: ContextMenuEntry[], subMenu: boolean }) => { + return ( +
+ {props.entries.map(entry => )} +
+ ) +}; + +class ContextMenuRenderer extends React.Component<{}, { entries: ContextMenuEntry[], pageX: number, pageY: number, callbackClose: () => void }> { + constructor(props) { + super(props); + + this.state = { + pageY: 0, + pageX: 0, + entries: [], + callbackClose: () => {} + } + } + + render() { + return ( + { + if(this.state.callbackClose) { + this.state.callbackClose(); + } + this.setState({ entries: [], callbackClose: undefined }); + }}> +
+ +
+
+ ) + } +} + +let uniqueIdIndex = 0; +function generateUniqueIds(entry: ContextMenuEntry) { + if(typeof entry.uniqueId !== "string") { + entry.uniqueId = "_" + (++uniqueIdIndex); + } + + if(entry.type === "normal" && entry.subMenu) { + entry.subMenu.forEach(generateUniqueIds); + } +} + +export let reactContextMenuInstance: ContextMenuFactory; +loader.register_task(Stage.JAVASCRIPT_INITIALIZING, { + priority: 80, + name: "context menu init", + function: async () => { + document.addEventListener("mousedown", globalMouseListener = event => { + if(refRenderer.current?.state.entries?.length) { + let target: HTMLElement = event.target as any; + while (target) { + if(target.classList.contains(cssStyle.container)) { + return; + } + + target = target.parentElement; + } + + closeContextMenu(); + } + }); + + globalContainer = document.createElement("div"); + globalContainer.classList.add(cssStyle.globalContainer); + document.body.append(globalContainer); + + ReactDOM.render(, globalContainer); + + reactContextMenuInstance = new class implements ContextMenuFactory { + spawnContextMenu(position: { pageX: number; pageY: number }, entries: ContextMenuEntry[]) { + entries.forEach(generateUniqueIds); + refRenderer.current?.setState({ + entries: entries, + pageX: position.pageX, + pageY: position.pageY + }); + } + + closeContextMenu() { + if(refRenderer.current?.state.entries?.length) { + refRenderer.current?.setState({ callbackClose: undefined, entries: [] }); + } + } + }; + + /* + setTimeout(() => { + spawnContextMenu({ pageX: 100, pageY: 100 }, [ + { + type: "normal", + label: { text: "test", bold: true }, + icon: ClientIcon.IsTalker + }, + { + type: "normal", + label: "test 2", + icon: ClientIcon.ServerGreen + }, + { + type: "separator" + }, + { + type: "normal", + label: "test 3", + subMenu: [ + { + type: "checkbox", + label: "test - cb", + checked: false + }, + { + type: "checkbox", + label: "test - cb 1", + checked: true + } + ] + }, + { + type: "normal", + label: "test 4", + subMenu: [ + { + type: "normal", + label: "test 1", + icon: ClientIcon.IsTalker + }, + { + type: "normal", + label: "test 2", + icon: ClientIcon.ServerGreen + }, + { + type: "separator" + }, + { + type: "normal", + label: "test 3" + }, + { + type: "normal", + label: "test 4", + subMenu: [ + { + type: "normal", + label: "test 1", + icon: ClientIcon.IsTalker + }, + { + type: "normal", + label: "test 2", + icon: ClientIcon.ServerGreen + }, + { + type: "separator" + }, + { + type: "normal", + label: "test 3" + }, + { + type: "normal", + label: "test 4" + } + ] + } + ] + } + ]); + }, 1000); + */ + } +}) diff --git a/shared/js/ui/context-menu/index.ts b/shared/js/ui/context-menu/index.ts new file mode 100644 index 00000000..fc1b621a --- /dev/null +++ b/shared/js/ui/context-menu/index.ts @@ -0,0 +1,69 @@ +import {RemoteIcon} from "tc-shared/file/Icons"; +import {ClientIcon} from "svg-sprites/client-icons"; + +import "./Ipc"; + +export type MenuEntryLabel = { + text: string, + bold?: boolean; +} | string; + +type MenuEntryClickable = { + uniqueId?: string, + label: MenuEntryLabel, + + enabled?: boolean; + visible?: boolean; + + click?: () => void; +} + +export type ContextMenuEntryNormal = { + type: "normal", + icon?: RemoteIcon | ClientIcon, + subMenu?: ContextMenuEntry[], +} & MenuEntryClickable; + +export type ContextMenuEntrySeparator = { + uniqueId?: string, + type: "separator", + visible?: boolean +} + +export type ContextMenuEntryCheckbox = { + type: "checkbox", + checked?: boolean; +} & MenuEntryClickable; + +export type ContextMenuEntry = ContextMenuEntryNormal | ContextMenuEntrySeparator | ContextMenuEntryCheckbox; + +export interface ContextMenuFactory { + spawnContextMenu(position: { pageX: number, pageY: number }, entries: ContextMenuEntry[], callbackClose?: () => void); + closeContextMenu(); +} + +let globalContextMenuFactory: ContextMenuFactory; +export function setGlobalContextMenuFactory(instance: ContextMenuFactory) { + if(globalContextMenuFactory) { + throw tr("the global context menu factory has already been set"); + } + globalContextMenuFactory = instance; +} + + +export function spawnContextMenu(position: { pageX: number, pageY: number }, entries: ContextMenuEntry[], callbackClose?: () => void) { + if(!globalContextMenuFactory) { + throw tr("missing global context menu factory"); + } + + globalContextMenuFactory.spawnContextMenu(position, entries, callbackClose); +} + + +export function closeContextMenu() { + if(!globalContextMenuFactory) { + throw tr("missing global context menu factory"); + } + + globalContextMenuFactory.closeContextMenu(); +} \ No newline at end of file diff --git a/shared/js/ui/elements/ContextMenu.ts b/shared/js/ui/elements/ContextMenu.ts index 8b004706..7065409d 100644 --- a/shared/js/ui/elements/ContextMenu.ts +++ b/shared/js/ui/elements/ContextMenu.ts @@ -1,4 +1,6 @@ -import * as $ from "jquery"; +import {closeContextMenu, ContextMenuEntry, spawnContextMenu} from "tc-shared/ui/context-menu"; +import {ClientIcon} from "svg-sprites/client-icons"; + export interface MenuEntry { callback?: () => void; type: MenuEntryType; @@ -75,153 +77,61 @@ export function set_provider(_provider: ContextMenuProvider) { provider.initialize(); } -class HTMLContextMenuProvider implements ContextMenuProvider { - private _global_click_listener: (event) => any; - private _context_menu: JQuery; - private _close_callbacks: (() => any)[] = []; - private _visible = false; - +class LegacyBridgeContextMenuProvider implements ContextMenuProvider { despawn_context_menu() { - if(!this._visible) - return; + closeContextMenu(); + } - let menu = this._context_menu || (this._context_menu = $(".context-menu")); - menu.animate({opacity: 0}, 100, () => menu.css("display", "none")); - this._visible = false; - for(const callback of this._close_callbacks) { - if(typeof(callback) !== "function") { - console.error(tr("Given close callback is not a function!. Callback: %o"), callback); - continue; - } - callback(); + finalize() { } + initialize() { } + + html_format_enabled(): boolean { + return false; + } + + private static mapEntry(entry: MenuEntry, closeListener: (() => void)[]) : ContextMenuEntry | undefined { + switch (entry.type) { + case MenuEntryType.CLOSE: + closeListener.push(entry.callback); + break; + + case MenuEntryType.CHECKBOX: + return { + type: "checkbox", + checked: entry.checkbox_checked, + label: typeof entry.name === "function" ? entry.name() : entry.name, + click: entry.callback, + enabled: !entry.disabled && !entry.invalidPermission, + visible: entry.visible + }; + + case MenuEntryType.ENTRY: + case MenuEntryType.SUB_MENU: + return { + type: "normal", + label: typeof entry.name === "function" ? entry.name() : entry.name, + click: entry.callback, + enabled: !entry.disabled && !entry.invalidPermission, + visible: entry.visible, + icon: entry.icon_class as ClientIcon, + subMenu: entry.sub_menu ? entry.sub_menu.map(entry => this.mapEntry(entry, closeListener)).filter(e => !!e) : undefined + }; + + case MenuEntryType.HR: + return { + type: "separator", + visible: entry.visible + }; + + default: + return undefined; } - this._close_callbacks = []; - } - - finalize() { - $(document).unbind('click', this._global_click_listener); - } - - initialize() { - this._global_click_listener = this.on_global_click.bind(this); - $(document).bind('click', this._global_click_listener); - } - - private on_global_click(event) { - //let menu = this._context_menu || (this._context_menu = $(".context-menu")); - - if(!this._visible) return; - if ($(event.target).parents(".context-menu").length == 0) { - this.despawn_context_menu(); - event.preventDefault(); - } - } - - private generate_tag(entry: MenuEntry) : JQuery { - if(entry.type == MenuEntryType.HR) { - return $.spawn("hr"); - } else if(entry.type == MenuEntryType.ENTRY) { - let icon = entry.icon_class; - if(!icon || icon.length == 0) icon = "icon_empty"; - else icon = "icon " + icon; - - let tag = $.spawn("div").addClass("entry"); - tag.append($.spawn("div").addClass(icon)); - tag.append($.spawn("div").html($.isFunction(entry.name) ? entry.name() : entry.name)); - - if(entry.disabled || entry.invalidPermission) - tag.addClass("disabled"); - else { - tag.on('click', () => { - if($.isFunction(entry.callback)) - entry.callback(); - entry.callback = undefined; /* for some reason despawn_context_menu() causes a second click event? */ - this.despawn_context_menu(); - }); - } - return tag; - } else if(entry.type == MenuEntryType.CHECKBOX) { - let checkbox = $.spawn("label").addClass("ccheckbox"); - $.spawn("input").attr("type", "checkbox").prop("checked", !!entry.checkbox_checked).appendTo(checkbox); - $.spawn("span").addClass("checkmark").appendTo(checkbox); - - let tag = $.spawn("div").addClass("entry"); - tag.append(checkbox); - tag.append($.spawn("div").html($.isFunction(entry.name) ? entry.name() : entry.name)); - - if(entry.disabled || entry.invalidPermission) - tag.addClass("disabled"); - else { - tag.on('click', () => { - if($.isFunction(entry.callback)) - entry.callback(); - entry.callback = undefined; /* for some reason despawn_context_menu() causes a second click event? */ - this.despawn_context_menu(); - }); - } - return tag; - } else if(entry.type == MenuEntryType.SUB_MENU) { - let icon = entry.icon_class; - if(!icon || icon.length == 0) icon = "icon_empty"; - else icon = "icon " + icon; - - let tag = $.spawn("div").addClass("entry").addClass("sub-container"); - tag.append($.spawn("div").addClass(icon)); - tag.append($.spawn("div").html($.isFunction(entry.name) ? entry.name() : entry.name)); - - tag.append($.spawn("div").addClass("arrow right")); - - if(entry.disabled || entry.invalidPermission) tag.addClass("disabled"); - else { - let menu = $.spawn("div").addClass("sub-menu").addClass("context-menu-container"); - for(const e of entry.sub_menu) { - if(typeof(entry.visible) === 'boolean' && !entry.visible) - continue; - menu.append(this.generate_tag(e)); - } - menu.appendTo(tag); - } - return tag; - } - return $.spawn("div").text("undefined"); } spawn_context_menu(x: number, y: number, ...entries: MenuEntry[]) { - this._visible = true; - - let menu_tag = this._context_menu || (this._context_menu = $(".context-menu")); - menu_tag.finish().empty().css("opacity", "0"); - - const menu_container = $.spawn("div").addClass("context-menu-container"); - this._close_callbacks = []; - - for(const entry of entries){ - if(typeof(entry.visible) === 'boolean' && !entry.visible) - continue; - - if(entry.type == MenuEntryType.CLOSE) { - if(entry.callback) - this._close_callbacks.push(entry.callback); - } else - menu_container.append(this.generate_tag(entry)); - } - - menu_tag.append(menu_container); - menu_tag.animate({opacity: 1}, 100).css("display", "block"); - - const width = menu_container.visible_width(); - if(x + width + 5 > window.innerWidth) - menu_container.addClass("left"); - - // In the right position (the mouse) - menu_tag.css({ - "top": y + "px", - "left": x + "px" - }); - } - - html_format_enabled(): boolean { - return true; + const closeCallbacks = []; + spawnContextMenu({ pageX: x, pageY: y }, entries.map(e => LegacyBridgeContextMenuProvider.mapEntry(e, closeCallbacks)).filter(e => !!e), () => closeCallbacks.forEach(callback => callback())); } } -set_provider(new HTMLContextMenuProvider()); \ No newline at end of file + +set_provider(new LegacyBridgeContextMenuProvider()); \ No newline at end of file diff --git a/shared/js/ui/react-elements/external-modal/PopoutController.ts b/shared/js/ui/react-elements/external-modal/PopoutController.ts index 4adcf76f..2472fd4a 100644 --- a/shared/js/ui/react-elements/external-modal/PopoutController.ts +++ b/shared/js/ui/react-elements/external-modal/PopoutController.ts @@ -1,4 +1,4 @@ -import {ChannelMessage, getInstance as getIPCInstance, IPCChannel} from "../../../ipc/BrowserIPC"; +import {getInstance as getIPCInstance} from "../../../ipc/BrowserIPC"; import {Settings, SettingsKey} from "../../../settings"; import { Controller2PopoutMessages, EventControllerBase, diff --git a/shared/js/ui/react-elements/external-modal/PopoutEntrypoint.ts b/shared/js/ui/react-elements/external-modal/PopoutEntrypoint.ts index ceece180..abb9ea36 100644 --- a/shared/js/ui/react-elements/external-modal/PopoutEntrypoint.ts +++ b/shared/js/ui/react-elements/external-modal/PopoutEntrypoint.ts @@ -14,6 +14,7 @@ import {setupJSRender} from "../../../ui/jsrender"; import "../../../file/RemoteAvatars"; import "../../../file/RemoteIcons"; +import "../../context-menu"; let modalRenderer: ModalRenderer; let modalInstance: AbstractModal;