Move functional tab pages out of separate webviews.

Signed-off-by: Anders Kaseorg <anders@zulip.com>
This commit is contained in:
Anders Kaseorg
2022-02-17 22:30:55 -08:00
parent b263997bed
commit 84849d2c84
14 changed files with 109 additions and 186 deletions

View File

@@ -1,5 +1,5 @@
import type {DNDSettings} from "./dnd-util";
import type {MenuProps, NavItem, ServerConf} from "./types";
import type {MenuProps, ServerConf} from "./types";
export interface MainMessage {
"clear-app-settings": () => void;
@@ -66,7 +66,6 @@ export interface RendererMessage {
"show-keyboard-shortcuts": () => void;
"show-notification-settings": () => void;
"switch-server-tab": (index: number) => void;
"switch-settings-nav": (navItem: NavItem) => void;
"tab-devtools": () => void;
"toggle-autohide-menubar": (
autoHideMenubar: boolean,

View File

@@ -1,21 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Zulip - About</title>
<style>
html,
body,
body > div {
margin: 0;
height: 100%;
}
</style>
</head>
<body>
<script>
const {AboutView} = require("./js/pages/about.js");
document.body.append(new AboutView().$view);
</script>
</body>
</html>

View File

@@ -304,27 +304,28 @@ body {
visibility: hidden;
}
webview {
webview,
.functional-view {
/* transition: opacity 0.3s ease-in; */
position: absolute;
width: 100%;
height: 100%;
flex-grow: 1;
display: flex;
flex-direction: column;
}
webview.onload {
transition: opacity 1s cubic-bezier(0.95, 0.05, 0.795, 0.035);
}
webview.active {
webview.active,
.functional-view.active {
opacity: 1;
z-index: 1;
visibility: visible;
}
webview.disabled {
webview.disabled,
.functional-view.disabled {
opacity: 0;
}

View File

@@ -5,13 +5,19 @@ import {generateNodeFromHTML} from "./base";
import type {TabProps} from "./tab";
import Tab from "./tab";
export interface FunctionalTabProps extends TabProps {
$view: Element;
}
export default class FunctionalTab extends Tab {
$view: Element;
$el: Element;
$closeButton?: Element;
constructor(props: TabProps) {
constructor({$view, ...props}: FunctionalTabProps) {
super(props);
this.$view = $view;
this.$el = generateNodeFromHTML(this.templateHTML());
if (this.props.name !== "Settings") {
this.props.$root.append(this.$el);
@@ -20,6 +26,23 @@ export default class FunctionalTab extends Tab {
}
}
override activate(): void {
super.activate();
this.$view.classList.add("active");
this.$view.classList.remove("disabled");
}
override deactivate(): void {
super.deactivate();
this.$view.classList.add("disabled");
this.$view.classList.remove("active");
}
override destroy(): void {
super.destroy();
this.$view.remove();
}
templateHTML(): HTML {
return html`
<div class="tab functional-tab" data-tab-id="${this.props.tabIndex}">

View File

@@ -5,20 +5,42 @@ import {ipcRenderer} from "../typed-ipc-renderer";
import {generateNodeFromHTML} from "./base";
import type {TabProps} from "./tab";
import Tab from "./tab";
import type WebView from "./webview";
export interface ServerTabProps extends TabProps {
webview: WebView;
}
export default class ServerTab extends Tab {
webview: WebView;
$el: Element;
$badge: Element;
constructor(props: TabProps) {
constructor({webview, ...props}: ServerTabProps) {
super(props);
this.webview = webview;
this.$el = generateNodeFromHTML(this.templateHTML());
this.props.$root.append(this.$el);
this.registerListeners();
this.$badge = this.$el.querySelector(".server-tab-badge")!;
}
override activate(): void {
super.activate();
this.webview.load();
}
override deactivate(): void {
super.deactivate();
this.webview.hide();
}
override destroy(): void {
super.destroy();
this.webview.$el!.remove();
}
templateHTML(): HTML {
return html`
<div class="tab" data-tab-id="${this.props.tabIndex}">

View File

@@ -1,7 +1,5 @@
import type {TabRole} from "../../../common/types";
import type WebView from "./webview";
export interface TabProps {
role: TabRole;
icon?: string;
@@ -12,19 +10,16 @@ export interface TabProps {
tabIndex: number;
onHover?: () => void;
onHoverOut?: () => void;
webview: WebView;
materialIcon?: string;
onDestroy?: () => void;
}
export default abstract class Tab {
props: TabProps;
webview: WebView;
abstract $el: Element;
constructor(props: TabProps) {
this.props = props;
this.webview = this.props.webview;
}
registerListeners(): void {
@@ -41,16 +36,13 @@ export default abstract class Tab {
activate(): void {
this.$el.classList.add("active");
this.webview.load();
}
deactivate(): void {
this.$el.classList.remove("active");
this.webview.hide();
}
destroy(): void {
this.$el.remove();
this.webview.$el!.remove();
}
}

View File

@@ -107,12 +107,7 @@ export default class WebView {
this.props.onTitleChange();
});
this.$el!.addEventListener("did-navigate-in-page", (event) => {
const isSettingPage = event.url.includes("renderer/preference.html");
if (isSettingPage) {
return;
}
this.$el!.addEventListener("did-navigate-in-page", () => {
this.canGoBackButton();
});
@@ -170,10 +165,7 @@ export default class WebView {
});
this.$el!.addEventListener("did-start-loading", () => {
const isSettingPage = this.props.url.includes("renderer/preference.html");
if (!isSettingPage) {
this.props.switchLoading(true, this.props.url);
}
});
this.$el!.addEventListener("did-stop-loading", () => {

View File

@@ -8,12 +8,13 @@ import type {DNDSettings} from "../../common/dnd-util";
import * as EnterpriseUtil from "../../common/enterprise-util";
import Logger from "../../common/logger-util";
import * as Messages from "../../common/messages";
import type {RendererMessage} from "../../common/typed-ipc";
import type {NavItem, ServerConf, TabData} from "../../common/types";
import FunctionalTab from "./components/functional-tab";
import ServerTab from "./components/server-tab";
import WebView from "./components/webview";
import {AboutView} from "./pages/about";
import {PreferenceView} from "./pages/preference/preference";
import {initializeTray} from "./tray";
import {ipcRenderer} from "./typed-ipc-renderer";
import * as DomainUtil from "./utils/domain-util";
@@ -41,7 +42,7 @@ const logger = new Logger({
const rendererDirectory = path.resolve(__dirname, "..");
type ServerOrFunctionalTab = ServerTab | FunctionalTab;
class ServerManagerView {
export class ServerManagerView {
$addServerButton: HTMLButtonElement;
$tabsContainer: Element;
$reloadButton: HTMLButtonElement;
@@ -66,6 +67,7 @@ class ServerManagerView {
functionalTabs: Map<string, number>;
tabIndex: number;
presetOrgs: string[];
preferenceView?: PreferenceView;
constructor() {
this.$addServerButton = document.querySelector("#add-tab")!;
this.$tabsContainer = document.querySelector("#tabs-container")!;
@@ -111,7 +113,7 @@ class ServerManagerView {
}
async init(): Promise<void> {
initializeTray();
initializeTray(this);
await this.loadProxy();
this.initDefaultSettings();
this.initSidebar();
@@ -532,16 +534,20 @@ class ServerManagerView {
openFunctionalTab(tabProps: {
name: string;
materialIcon: string;
url: string;
makeView: () => Element;
destroyView: () => void;
}): void {
if (this.functionalTabs.has(tabProps.name)) {
this.activateTab(this.functionalTabs.get(tabProps.name)!);
return;
}
this.functionalTabs.set(tabProps.name, this.tabs.length);
const index = this.tabs.length;
this.functionalTabs.set(tabProps.name, index);
const tabIndex = this.getTabIndex();
const $view = tabProps.makeView();
this.$webviewsContainer.append($view);
this.tabs.push(
new FunctionalTab({
@@ -549,45 +555,14 @@ class ServerManagerView {
materialIcon: tabProps.materialIcon,
name: tabProps.name,
$root: this.$tabsContainer,
index: this.functionalTabs.get(tabProps.name)!,
index,
tabIndex,
onClick: this.activateTab.bind(
this,
this.functionalTabs.get(tabProps.name)!,
),
onDestroy: this.destroyTab.bind(
this,
tabProps.name,
this.functionalTabs.get(tabProps.name)!,
),
webview: new WebView({
$root: this.$webviewsContainer,
index: this.functionalTabs.get(tabProps.name)!,
tabIndex,
url: tabProps.url,
role: "function",
isActive: () =>
this.functionalTabs.get(tabProps.name) === this.activeTabIndex,
switchLoading: (loading: boolean, url: string) => {
if (loading) {
this.loading.add(url);
} else {
this.loading.delete(url);
}
const tab = this.tabs[this.activeTabIndex];
this.showLoading(
tab instanceof ServerTab &&
this.loading.has(tab.webview.props.url),
);
onClick: this.activateTab.bind(this, index),
onDestroy: () => {
this.destroyTab(tabProps.name, index);
tabProps.destroyView();
},
onNetworkError: async (index: number) => {
await this.openNetworkTroubleshooting(index);
},
onTitleChange: this.updateBadge.bind(this),
nodeIntegration: true,
preload: false,
}),
$view,
}),
);
@@ -602,20 +577,33 @@ class ServerManagerView {
this.openFunctionalTab({
name: "Settings",
materialIcon: "settings",
url: `file://${rendererDirectory}/preference.html#${nav}`,
makeView: () => {
this.preferenceView = new PreferenceView();
this.preferenceView.$view.classList.add("functional-view");
return this.preferenceView.$view;
},
destroyView: () => {
this.preferenceView!.destroy();
this.preferenceView = undefined;
},
});
this.$settingsButton.classList.add("active");
await this.tabs[this.functionalTabs.get("Settings")!].webview.send(
"switch-settings-nav",
nav,
);
this.preferenceView!.handleNavigation(nav);
}
openAbout(): void {
let aboutView: AboutView;
this.openFunctionalTab({
name: "About",
materialIcon: "sentiment_very_satisfied",
url: `file://${rendererDirectory}/about.html`,
makeView: () => {
aboutView = new AboutView();
aboutView.$view.classList.add("functional-view");
return aboutView.$view;
},
destroyView: () => {
aboutView.destroy();
},
});
}
@@ -768,16 +756,6 @@ class ServerManagerView {
ipcRenderer.send("update-badge", messageCountAll);
}
updateGeneralSettings<Channel extends keyof RendererMessage>(
channel: Channel,
...args: Parameters<RendererMessage[Channel]>
): void {
if (this.getActiveWebview()) {
const webContentsId = this.getActiveWebview().getWebContentsId();
ipcRenderer.sendTo(webContentsId, channel, ...args);
}
}
toggleSidebar(show: boolean): void {
if (show) {
this.$sidebar.classList.remove("sidebar-hide");
@@ -802,12 +780,6 @@ class ServerManagerView {
return !(url.endsWith("/login/") || tab.webview.loading);
}
getActiveWebview(): Electron.WebviewTag {
const selector = "webview:not(.disabled)";
const webview: Electron.WebviewTag = document.querySelector(selector)!;
return webview;
}
addContextMenu($serverImg: HTMLElement, index: number): void {
$serverImg.addEventListener("contextmenu", (event) => {
event.preventDefault();
@@ -1012,9 +984,6 @@ class ServerManagerView {
ipcRenderer.on("toggle-sidebar", (event: Event, show: boolean) => {
// Toggle the left sidebar
this.toggleSidebar(show);
// Toggle sidebar switch in the general settings
this.updateGeneralSettings("toggle-sidebar", show);
});
ipcRenderer.on("toggle-silent", (event: Event, state: boolean) => {
@@ -1040,14 +1009,7 @@ class ServerManagerView {
tabs: this.tabsForIpc,
activeTabIndex: this.activeTabIndex,
});
return;
}
this.updateGeneralSettings(
"toggle-autohide-menubar",
autoHideMenubar,
updateMenu,
);
},
);
@@ -1060,8 +1022,6 @@ class ServerManagerView {
"toggle-silent",
newSettings.silent ?? false,
);
const webContentsId = this.getActiveWebview().getWebContentsId();
ipcRenderer.sendTo(webContentsId, "toggle-dnd", state, newSettings);
},
);

View File

@@ -15,6 +15,7 @@ export class PreferenceView {
private readonly $shadow: ShadowRoot;
private readonly $settingsContainer: Element;
private readonly nav: Nav;
private navItem: NavItem = "General";
constructor() {
this.$view = document.createElement("div");
@@ -49,21 +50,15 @@ export class PreferenceView {
onItemSelected: this.handleNavigation,
});
const navItem =
this.nav.navItems.find(
(navItem) => window.location.hash === `#${navItem}`,
) ?? "General";
this.handleNavigation(navItem);
ipcRenderer.on("switch-settings-nav", this.handleSwitchSettingsNav);
ipcRenderer.on("toggle-sidebar", this.handleToggleSidebar);
ipcRenderer.on("toggle-autohide-menubar", this.handleToggleMenubar);
ipcRenderer.on("toggle-tray", this.handleToggleTray);
ipcRenderer.on("toggle-dnd", this.handleToggleDnd);
this.handleNavigation(this.navItem);
}
handleNavigation = (navItem: NavItem): void => {
this.navItem = navItem;
this.nav.select(navItem);
switch (navItem) {
case "AddServer":
@@ -104,11 +99,13 @@ export class PreferenceView {
window.location.hash = `#${navItem}`;
};
handleToggleTray(state: boolean) {
this.handleToggle("tray-option", state);
}
destroy(): void {
ipcRenderer.off("switch-settings-nav", this.handleSwitchSettingsNav);
ipcRenderer.off("toggle-sidebar", this.handleToggleSidebar);
ipcRenderer.off("toggle-autohide-menubar", this.handleToggleMenubar);
ipcRenderer.off("toggle-tray", this.handleToggleTray);
ipcRenderer.off("toggle-dnd", this.handleToggleDnd);
}
@@ -121,13 +118,6 @@ export class PreferenceView {
}
}
private readonly handleSwitchSettingsNav = (
_event: Event,
navItem: NavItem,
) => {
this.handleNavigation(navItem);
};
private readonly handleToggleSidebar = (_event: Event, state: boolean) => {
this.handleToggle("sidebar-option", state);
};
@@ -136,10 +126,6 @@ export class PreferenceView {
this.handleToggle("menubar-option", state);
};
private readonly handleToggleTray = (_event: Event, state: boolean) => {
this.handleToggle("tray-option", state);
};
private readonly handleToggleDnd = (
_event: Event,
_state: boolean,

View File

@@ -1,10 +1,11 @@
import type {NativeImage, WebviewTag} from "electron";
import type {NativeImage} from "electron";
import {remote} from "electron";
import path from "path";
import * as ConfigUtil from "../../common/config-util";
import type {RendererMessage} from "../../common/typed-ipc";
import type {ServerManagerView} from "./main";
import {ipcRenderer} from "./typed-ipc-renderer";
const {Tray, Menu, nativeImage, BrowserWindow} = remote;
@@ -168,7 +169,7 @@ const createTray = function (): void {
}
};
export function initializeTray() {
export function initializeTray(serverManagerView: ServerManagerView) {
ipcRenderer.on("destroytray", (_event: Event) => {
if (!tray) {
return;
@@ -224,12 +225,7 @@ export function initializeTray() {
ConfigUtil.setConfigItem("trayIcon", true);
}
const webview = document.querySelector<WebviewTag>(
"webview:not([class*=disabled])",
);
if (webview !== null) {
ipcRenderer.sendTo(webview.getWebContentsId(), "toggle-tray", state);
}
serverManagerView.preferenceView?.handleToggleTray(state);
}
ipcRenderer.on("toggletray", toggleTray);

View File

@@ -1,24 +0,0 @@
<!DOCTYPE html>
<html lang="en" class="responsive desktop">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta name="viewport" content="width=device-width" />
<title>Zulip - Settings</title>
<link rel="stylesheet" href="css/fonts.css" />
<style>
html,
body,
body > div {
margin: 0;
height: 100%;
}
</style>
</head>
<body>
<zd-preference-view></zd-preference-view>
<script>
const {PreferenceView} = require("./js/pages/preference/preference.js");
document.body.append(new PreferenceView().$view);
</script>
</body>
</html>

View File

@@ -16,8 +16,7 @@ test("app runs", async (t) => {
const mainWindow = await take(windows);
t.equal(await mainWindow.title(), "Zulip");
const mainWebview = await take(windows);
await mainWebview.waitForSelector("#connect");
await mainWindow.waitForSelector("#connect");
} finally {
await setup.endTest(app);
}

View File

@@ -16,9 +16,8 @@ test("add-organization", async (t) => {
const mainWindow = await take(windows);
t.equal(await mainWindow.title(), "Zulip");
const mainWebview = await take(windows);
await mainWebview.fill(".setting-input-value", "chat.zulip.org");
await mainWebview.click("#connect");
await mainWindow.fill(".setting-input-value", "chat.zulip.org");
await mainWindow.click("#connect");
const orgWebview = await take(windows);
await orgWebview.waitForSelector("#id_username");

View File

@@ -18,8 +18,7 @@ test("new-org-link", async (t) => {
const mainWindow = await take(windows);
t.equal(await mainWindow.title(), "Zulip");
const mainWebview = await take(windows);
await mainWebview.click("#open-create-org-link");
await mainWindow.click("#open-create-org-link");
} finally {
await setup.endTest(app);
}