mirror of
				https://github.com/zulip/zulip-desktop.git
				synced 2025-11-04 14:03:27 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			302 lines
		
	
	
		
			8.0 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			302 lines
		
	
	
		
			8.0 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';
 | 
						|
 | 
						|
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: any, url: string) => void;
 | 
						|
	onNetworkError: (index: number) => void;
 | 
						|
	nodeIntegration: boolean;
 | 
						|
	preload: boolean;
 | 
						|
	onTitleChange: any;
 | 
						|
	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, '}javascript=yes">
 | 
						|
				</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', () => {
 | 
						|
			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: any[]): Promise<void> {
 | 
						|
		await this.domReady;
 | 
						|
		await this.$el.send(channel, ...parameters);
 | 
						|
	}
 | 
						|
}
 |