mirror of
https://github.com/zulip/zulip-desktop.git
synced 2025-10-29 02:53:40 +00:00
* Using electron built-in spellchecker * Added the custom context menu Co-authored-by: Anders Kaseorg <anders@zulipchat.com> Fixes: #504
312 lines
8.3 KiB
TypeScript
312 lines
8.3 KiB
TypeScript
import {ipcRenderer, remote} from 'electron';
|
|
|
|
import path from 'path';
|
|
import fs from 'fs';
|
|
import * as ConfigUtil from '../utils/config-util';
|
|
import * as SystemUtil from '../utils/system-util';
|
|
import BaseComponent from './base';
|
|
import handleExternalLink from './handle-external-link';
|
|
import {contextMenu} from './context-menu';
|
|
|
|
const {app, dialog} = remote;
|
|
|
|
const shouldSilentWebview = ConfigUtil.getConfigItem('silent');
|
|
|
|
interface WebViewProps {
|
|
$root: Element;
|
|
index: number;
|
|
tabIndex: number;
|
|
url: string;
|
|
role: string;
|
|
name: string;
|
|
isActive: () => boolean;
|
|
switchLoading: (loading: boolean, url: string) => void;
|
|
onNetworkError: (index: number) => void;
|
|
nodeIntegration: boolean;
|
|
preload: boolean;
|
|
onTitleChange: () => void;
|
|
hasPermission?: (origin: string, permission: string) => boolean;
|
|
}
|
|
|
|
export default class WebView extends BaseComponent {
|
|
props: WebViewProps;
|
|
zoomFactor: number;
|
|
badgeCount: number;
|
|
loading: boolean;
|
|
customCSS: string;
|
|
$webviewsContainer: DOMTokenList;
|
|
$el: Electron.WebviewTag;
|
|
domReady?: Promise<void>;
|
|
|
|
constructor(props: WebViewProps) {
|
|
super();
|
|
|
|
this.props = props;
|
|
this.zoomFactor = 1;
|
|
this.loading = true;
|
|
this.badgeCount = 0;
|
|
this.customCSS = ConfigUtil.getConfigItem('customCSS');
|
|
this.$webviewsContainer = document.querySelector('#webviews-container').classList;
|
|
}
|
|
|
|
template(): string {
|
|
return `<webview
|
|
class="disabled"
|
|
data-tab-id="${this.props.tabIndex}"
|
|
src="${this.props.url}"
|
|
${this.props.nodeIntegration ? 'nodeIntegration' : ''}
|
|
${this.props.preload ? 'preload="js/preload.js"' : ''}
|
|
partition="persist:webviewsession"
|
|
name="${this.props.name}"
|
|
webpreferences="
|
|
${this.props.nodeIntegration ? '' : 'contextIsolation,'}
|
|
${ConfigUtil.getConfigItem('enableSpellchecker') ? 'spellcheck,' : ''}
|
|
javascript
|
|
">
|
|
</webview>`;
|
|
}
|
|
|
|
init(): void {
|
|
this.$el = this.generateNodeFromTemplate(this.template()) as Electron.WebviewTag;
|
|
this.domReady = new Promise(resolve => {
|
|
this.$el.addEventListener('dom-ready', () => resolve(), true);
|
|
});
|
|
this.props.$root.append(this.$el);
|
|
|
|
this.registerListeners();
|
|
}
|
|
|
|
registerListeners(): void {
|
|
this.$el.addEventListener('new-window', event => {
|
|
handleExternalLink.call(this, event);
|
|
});
|
|
|
|
if (shouldSilentWebview) {
|
|
this.$el.addEventListener('dom-ready', () => {
|
|
this.$el.setAudioMuted(true);
|
|
});
|
|
}
|
|
|
|
this.$el.addEventListener('page-title-updated', event => {
|
|
const {title} = event;
|
|
this.badgeCount = this.getBadgeCount(title);
|
|
this.props.onTitleChange();
|
|
});
|
|
|
|
this.$el.addEventListener('did-navigate-in-page', event => {
|
|
const isSettingPage = event.url.includes('renderer/preference.html');
|
|
if (isSettingPage) {
|
|
return;
|
|
}
|
|
|
|
this.canGoBackButton();
|
|
});
|
|
|
|
this.$el.addEventListener('did-navigate', () => {
|
|
this.canGoBackButton();
|
|
});
|
|
|
|
this.$el.addEventListener('page-favicon-updated', event => {
|
|
const {favicons} = event;
|
|
|
|
// This returns a string of favicons URL. If there is a PM counts in unread messages then the URL would be like
|
|
// https://chat.zulip.org/static/images/favicon/favicon-pms.png
|
|
if (favicons[0].indexOf('favicon-pms') > 0 && process.platform === 'darwin') {
|
|
// This api is only supported on macOS
|
|
app.dock.setBadge('●');
|
|
// Bounce the dock
|
|
if (ConfigUtil.getConfigItem('dockBouncing')) {
|
|
app.dock.bounce();
|
|
}
|
|
}
|
|
});
|
|
|
|
this.$el.addEventListener('dom-ready', () => {
|
|
const webContents = remote.webContents.fromId(this.$el.getWebContentsId());
|
|
webContents.addListener('context-menu', (event, menuParameters) => {
|
|
contextMenu(webContents, event, menuParameters);
|
|
});
|
|
|
|
if (this.props.role === 'server') {
|
|
this.$el.classList.add('onload');
|
|
}
|
|
|
|
this.loading = false;
|
|
this.props.switchLoading(false, this.props.url);
|
|
this.show();
|
|
|
|
// Refocus text boxes after reload
|
|
// Remove when upstream issue https://github.com/electron/electron/issues/14474 is fixed
|
|
this.$el.blur();
|
|
this.$el.focus();
|
|
});
|
|
|
|
this.$el.addEventListener('did-fail-load', event => {
|
|
const {errorDescription} = event;
|
|
const hasConnectivityErr = SystemUtil.connectivityERR.includes(errorDescription);
|
|
if (hasConnectivityErr) {
|
|
console.error('error', errorDescription);
|
|
if (!this.props.url.includes('network.html')) {
|
|
this.props.onNetworkError(this.props.index);
|
|
}
|
|
}
|
|
});
|
|
|
|
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', () => {
|
|
this.props.switchLoading(false, this.props.url);
|
|
});
|
|
}
|
|
|
|
getBadgeCount(title: string): number {
|
|
const messageCountInTitle = (/\((\d+)\)/).exec(title);
|
|
return messageCountInTitle ? Number(messageCountInTitle[1]) : 0;
|
|
}
|
|
|
|
showNotificationSettings(): void {
|
|
ipcRenderer.sendTo(this.$el.getWebContentsId(), 'show-notification-settings');
|
|
}
|
|
|
|
show(): void {
|
|
// Do not show WebView if another tab was selected and this tab should be in background.
|
|
if (!this.props.isActive()) {
|
|
return;
|
|
}
|
|
|
|
// To show or hide the loading indicator in the the active tab
|
|
if (this.loading) {
|
|
this.$webviewsContainer.remove('loaded');
|
|
} else {
|
|
this.$webviewsContainer.add('loaded');
|
|
}
|
|
|
|
this.$el.classList.remove('disabled');
|
|
this.$el.classList.add('active');
|
|
setTimeout(() => {
|
|
if (this.props.role === 'server') {
|
|
this.$el.classList.remove('onload');
|
|
}
|
|
}, 1000);
|
|
this.focus();
|
|
this.props.onTitleChange();
|
|
// Injecting preload css in webview to override some css rules
|
|
(async () => this.$el.insertCSS(fs.readFileSync(path.join(__dirname, '/../../css/preload.css'), 'utf8')))();
|
|
|
|
// Get customCSS again from config util to avoid warning user again
|
|
this.customCSS = ConfigUtil.getConfigItem('customCSS');
|
|
if (this.customCSS) {
|
|
if (!fs.existsSync(this.customCSS)) {
|
|
this.customCSS = null;
|
|
ConfigUtil.setConfigItem('customCSS', null);
|
|
|
|
const errorMessage = 'The custom css previously set is deleted!';
|
|
dialog.showErrorBox('custom css file deleted!', errorMessage);
|
|
return;
|
|
}
|
|
|
|
(async () => this.$el.insertCSS(fs.readFileSync(path.resolve(__dirname, this.customCSS), 'utf8')))();
|
|
}
|
|
}
|
|
|
|
focus(): void {
|
|
// Focus Webview and it's contents when Window regain focus.
|
|
const webContents = this.$el.getWebContents();
|
|
// HACK: webContents.isFocused() seems to be true even without the element
|
|
// being in focus. So, we check against `document.activeElement`.
|
|
if (webContents && this.$el !== document.activeElement) {
|
|
// HACK: Looks like blur needs to be called on the previously focused
|
|
// element to transfer focus correctly, in Electron v3.0.10
|
|
// See https://github.com/electron/electron/issues/15718
|
|
(document.activeElement as HTMLElement).blur();
|
|
this.$el.focus();
|
|
webContents.focus();
|
|
}
|
|
}
|
|
|
|
hide(): void {
|
|
this.$el.classList.add('disabled');
|
|
this.$el.classList.remove('active');
|
|
}
|
|
|
|
load(): void {
|
|
if (this.$el) {
|
|
this.show();
|
|
} else {
|
|
this.init();
|
|
}
|
|
}
|
|
|
|
zoomIn(): void {
|
|
this.zoomFactor += 0.1;
|
|
this.$el.setZoomFactor(this.zoomFactor);
|
|
}
|
|
|
|
zoomOut(): void {
|
|
this.zoomFactor -= 0.1;
|
|
this.$el.setZoomFactor(this.zoomFactor);
|
|
}
|
|
|
|
zoomActualSize(): void {
|
|
this.zoomFactor = 1;
|
|
this.$el.setZoomFactor(this.zoomFactor);
|
|
}
|
|
|
|
logOut(): void {
|
|
ipcRenderer.sendTo(this.$el.getWebContentsId(), 'logout');
|
|
}
|
|
|
|
showShortcut(): void {
|
|
ipcRenderer.sendTo(this.$el.getWebContentsId(), 'shortcut');
|
|
}
|
|
|
|
openDevTools(): void {
|
|
this.$el.openDevTools();
|
|
}
|
|
|
|
back(): void {
|
|
if (this.$el.canGoBack()) {
|
|
this.$el.goBack();
|
|
this.focus();
|
|
}
|
|
}
|
|
|
|
canGoBackButton(): void {
|
|
const $backButton = document.querySelector('#actions-container #back-action');
|
|
if (this.$el.canGoBack()) {
|
|
$backButton.classList.remove('disable');
|
|
} else {
|
|
$backButton.classList.add('disable');
|
|
}
|
|
}
|
|
|
|
forward(): void {
|
|
if (this.$el.canGoForward()) {
|
|
this.$el.goForward();
|
|
}
|
|
}
|
|
|
|
reload(): void {
|
|
this.hide();
|
|
// Shows the loading indicator till the webview is reloaded
|
|
this.$webviewsContainer.remove('loaded');
|
|
this.loading = true;
|
|
this.props.switchLoading(true, this.props.url);
|
|
this.$el.reload();
|
|
}
|
|
|
|
forceLoad(): void {
|
|
this.init();
|
|
}
|
|
|
|
async send(channel: string, ...parameters: unknown[]): Promise<void> {
|
|
await this.domReady;
|
|
await this.$el.send(channel, ...parameters);
|
|
}
|
|
}
|