/** * Default config for all webviews created */ Ext.define("Rambox.ux.WebView", { extend: "Ext.panel.Panel", xtype: "webview", requires: [ "Rambox.util.Format", "Rambox.util.Notifier", "Rambox.util.UnreadCounter", "Rambox.util.IconLoader", ], // private zoomLevel: 0, currentUnreadCount: 0, // CONFIG hideMode: "offsets", initComponent: function (config) { var me = this; function getLocation(href) { var match = href.match( /^(https?\:)\/\/(([^:\/?#]*)(?:\:([0-9]+))?)(\/[^?#]*)(\?[^#]*|)(#.*|)$/ ); return ( match && { protocol: match[1], host: match[2], hostname: match[3], port: match[4], pathname: match[5], search: match[6], hash: match[7], } ); } const prefConfig = ipc.sendSync("getConfig"); Ext.apply(me, { items: me.webViewConstructor(), title: prefConfig.hide_tabbar_labels ? "" : me.record.get("tabname") ? me.record.get("name") : "", icon: me.record.get("type") === "custom" ? me.record.get("logo") === "" ? "resources/icons/custom.png" : me.record.get("logo") : "https://firebasestorage.googleapis.com/v0/b/rambox-d1326.appspot.com/o/services%2F" + me.record.get("logo") + "?alt=media", src: me.record.get("url"), type: me.record.get("type"), align: me.record.get("align"), notifications: me.record.get("notifications"), muted: me.record.get("muted"), tabConfig: { listeners: { afterrender: function (btn) { btn.el.on("contextmenu", function (e) { btn.showMenu("contextmenu"); e.stopEvent(); }); }, scope: me, }, clickEvent: "", style: !me.record.get("enabled") ? "-webkit-filter: grayscale(1)" : "", menu: { plain: true, items: [ { xtype: "toolbar", items: [ { xtype: "segmentedbutton", allowToggle: false, flex: 1, items: [ { text: "Back", glyph: "xf053@FontAwesome", flex: 1, scope: me, handler: me.goBack, }, { text: "Forward", glyph: "xf054@FontAwesome", iconAlign: "right", flex: 1, scope: me, handler: me.goForward, }, ], }, ], }, "-", { text: "Zoom In", glyph: "xf00e@FontAwesome", scope: me, handler: me.zoomIn, }, { text: "Zoom Out", glyph: "xf010@FontAwesome", scope: me, handler: me.zoomOut, }, { text: "Reset Zoom", glyph: "xf002@FontAwesome", scope: me, handler: me.resetZoom, }, "-", { text: locale["app.webview[0]"], glyph: "xf021@FontAwesome", scope: me, handler: me.reloadService, }, "-", { text: locale["app.webview[3]"], glyph: "xf121@FontAwesome", scope: me, handler: me.toggleDevTools, }, ], }, }, tbar: { itemId: "searchBar", hidden: true, items: [ "->", { xtype: "textfield", emptyText: "Search...", listeners: { scope: me, change: me.doSearchText, specialkey: function (field, e) { if (e.getKey() === e.ENTER) return me.doSearchText( field, field.getValue(), null, null, true ); if (e.getKey() === e.ESC) return me.showSearchBox(false); }, }, }, { xtype: "displayfield", }, { xtype: "segmentedbutton", allowMultiple: false, allowToggle: false, items: [ { glyph: "xf053@FontAwesome", handler: function () { var field = this.up("toolbar").down("textfield"); me.doSearchText(field, field.getValue(), null, null, false); }, }, { glyph: "xf054@FontAwesome", handler: function () { var field = this.up("toolbar").down("textfield"); me.doSearchText(field, field.getValue(), null, null, true); }, }, ], }, { xtype: "button", glyph: "xf00d@FontAwesome", handler: function () { me.showSearchBox(false); }, }, ], }, listeners: { afterrender: me.onAfterRender, beforedestroy: me.onBeforeDestroy, }, }); if (me.record.get("statusbar")) { Ext.apply(me, { bbar: me.statusBarConstructor(false), }); } else { me.items.push(me.statusBarConstructor(true)); } me.callParent(config); }, onBeforeDestroy: function () { var me = this; me.setUnreadCount(0); }, webViewConstructor: function (enabled) { var me = this; var cfg; enabled = enabled || me.record.get("enabled"); if (!enabled) { cfg = { xtype: "container", html: "

Service Disabled

", style: "text-align:center;", padding: 100, }; } else { cfg = [ { xtype: "component", cls: "webview", hideMode: "offsets", autoRender: true, autoShow: true, autoEl: { tag: "webview", src: me.record.get("url"), style: "width:100%;height:100%;visibility:visible;", partition: "persist:" + me.record.get("type") + "_" + me.id.replace("tab_", "") + (localStorage.getItem("id_token") ? "_" + Ext.decode(localStorage.getItem("profile")).sub : ""), plugins: "true", allowtransparency: "on", autosize: "on", webpreferences: "nativeWindowOpen=yes, spellcheck=no, contextIsolation=no", allowpopups: "on", // ,disablewebsecurity: 'on' // Disabled because some services (Like Google Drive) dont work with this enabled useragent: me.getUserAgent(), preload: "./resources/js/rambox-service-api.js", }, }, ]; } return cfg; }, getUserAgent: function () { var ua = ipc.sendSync("getConfig").user_agent ? ipc.sendSync("getConfig").user_agent : Ext.getStore("ServicesList").getById(this.record.get("type")) ? Ext.getStore("ServicesList") .getById(this.record.get("type")) .get("userAgent") : ""; return ua.length === 0 ? window.clientInformation.userAgent .replace(/Rambox\/([0-9]\.?)+\s/gi, "") .replace(/Electron\/([0-9]\.?)+\s/gi, "") : ua; }, statusBarConstructor: function (floating) { var me = this; return { xtype: "statusbar", id: me.id + "statusbar", hidden: !me.record.get("statusbar"), keep: me.record.get("statusbar"), y: floating ? "-18px" : "auto", height: 19, dock: "bottom", defaultText: ' Ready', busyIconCls: "", busyText: ' ' + locale["app.webview[4]"], items: [ { xtype: "tbtext", itemId: "url", }, { xtype: "button", glyph: "xf00d@FontAwesome", scale: "small", ui: "decline", padding: 0, scope: me, hidden: floating, handler: me.closeStatusBar, tooltip: { text: "Close statusbar until next time", mouseOffset: [0, -60], }, }, ], }; }, onAfterRender: function () { var me = this; if (!me.record.get("enabled")) return; var webview = me.getWebView(); me.errorCodeLog = []; // Google Analytics Event ga_storage._trackEvent("Services", "load", me.type, 1, true); // Notifications in Webview me.setNotifications( localStorage.getItem("locked") || JSON.parse(localStorage.getItem("dontDisturb")) ? false : me.record.get("notifications") ); // Show and hide spinner when is loading webview.addEventListener("did-start-loading", function () { console.info("Start loading...", me.src); require("electron") .remote.webContents.fromId(webview.getWebContentsId()) .session.webRequest.onBeforeSendHeaders((details, callback) => { Rambox.app.config.googleURLs.forEach((loginURL) => { if (details.url.indexOf(loginURL) > -1) details.requestHeaders["User-Agent"] = "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:70.0) Gecko/20100101 Firefox/70.0"; }); callback({ cancel: false, requestHeaders: details.requestHeaders }); }); if (!me.down("statusbar").closed || !me.down("statusbar").keep) me.down("statusbar").show(); me.down("statusbar").showBusy(); }); webview.addEventListener("did-stop-loading", function () { me.down("statusbar").clearStatus({ useDefaults: true }); if (!me.down("statusbar").keep) me.down("statusbar").hide(); }); webview.addEventListener("did-finish-load", function (e) { Rambox.app.setTotalServicesLoaded( Rambox.app.getTotalServicesLoaded() + 1 ); // Apply saved zoom level webview.setZoomLevel(me.record.get("zoomLevel")); // Fix cursor sometimes dissapear let currentTab = Ext.cq1("app-main").getActiveTab(); if (currentTab.id === me.id) { webview.blur(); webview.focus(); } // Set special icon for some service (like Slack) Rambox.util.IconLoader.loadServiceIconUrl(me, webview); }); // On search text webview.addEventListener("found-in-page", function (e) { me.onSearchText(e.result); }); // On search text webview.addEventListener("did-fail-load", function (e) { console.info("The service fail at loading", me.src, e); if (me.record.get("disableAutoReloadOnFail") || !e.isMainFrame) return; me.errorCodeLog.push(e.errorCode); var attempt = me.errorCodeLog.filter(function (code) { return code === e.errorCode; }); // Error codes: https://cs.chromium.org/chromium/src/net/base/net_error_list.h var msg = []; msg[-2] = "NET error: failed."; msg[-3] = "An operation was aborted (due to user action)"; msg[-7] = "Connection timeout."; msg[-21] = "Network change."; msg[-100] = "The connection was reset. Check your internet connection."; msg[-101] = "The connection was reset. Check your internet connection."; msg[-105] = "Name not resolved. Check your internet connection."; msg[-106] = "There is no active internet connection."; msg[-118] = "Connection timed out. Check your internet connection."; msg[-130] = "Proxy connection failed. Please, check the proxy configuration."; msg[-300] = "The URL is invalid."; msg[-324] = "Empty response. Check your internet connection."; switch (e.errorCode) { case 0: break; case -3: // An operation was aborted (due to user action) I think that gmail an other pages that use iframes stop some of them making this error fired if (attempt.length <= 4) return; setTimeout(() => me.reloadService(me), 200); me.errorCodeLog = []; break; case -2: case -7: case -21: case -118: case -324: case -100: case -101: case -105: attempt.length > 4 ? me.onFailLoad(msg[e.errorCode]) : setTimeout(() => me.reloadService(me), 2000); break; case -106: me.onFailLoad(msg[e.errorCode]); break; case -130: // Could not create a connection to the proxy server. An error occurred // either in resolving its name, or in connecting a socket to it. // Note that this does NOT include failures during the actual "CONNECT" method // of an HTTP proxy. case -300: attempt.length > 4 ? me.onFailLoad(msg[e.errorCode]) : me.reloadService(me); break; } }); // Open links in default browser webview.addEventListener("new-window", function (e) { e.preventDefault(); const protocol = require("url").parse(e.url).protocol; // Block some Deep links to prevent that open its app (Ex: Slack) if (["slack:"].includes(protocol)) return; // Allow Deep links if (!["http:", "https:", "about:"].includes(protocol)) return require("electron").shell.openExternal(e.url); }); webview.addEventListener("will-navigate", function (e, url) { e.preventDefault(); }); let eventsOnDom = false; webview.addEventListener("dom-ready", function (e) { // Mute Webview if ( me.record.get("muted") || localStorage.getItem("locked") || JSON.parse(localStorage.getItem("dontDisturb")) ) me.setAudioMuted(true, true); var js_inject = ""; // Injected code to detect new messages if (me.record) { var js_unread = Ext.getStore("ServicesList").getById( me.record.get("type") ) ? Ext.getStore("ServicesList") .getById(me.record.get("type")) .get("js_unread") : ""; js_unread = js_unread + me.record.get("js_unread"); if (js_unread !== "") { console.groupCollapsed( me.record.get("type").toUpperCase() + " - JS Injected to Detect New Messages" ); console.info(me.type); console.log(js_unread); js_inject += js_unread; } } // Prevent Title blinking (some services have) and only allow when the title have an unread regex match: "(3) Title" if ( Ext.getStore("ServicesList").getById(me.record.get("type")) ? Ext.getStore("ServicesList") .getById(me.record.get("type")) .get("titleBlink") : false ) { var js_preventBlink = 'var originalTitle=document.title;Object.defineProperty(document,"title",{configurable:!0,set:function(a){null===a.match(new RegExp("[(]([0-9•]+)[)][ ](.*)","g"))&&a!==originalTitle||(document.getElementsByTagName("title")[0].innerHTML=a)},get:function(){return document.getElementsByTagName("title")[0].innerHTML}});'; console.log(js_preventBlink); js_inject += js_preventBlink; } console.groupEnd(); // Scroll always to top (bug) js_inject += "document.body.scrollTop=0;"; // Handles Certificate Errors require("electron") .remote.webContents.fromId(webview.getWebContentsId()) .on( "certificate-error", function (event, url, error, certificate, callback) { if (me.record.get("trust")) { event.preventDefault(); callback(true); } else { callback(false); } me.down("statusbar").keep = true; me.down("statusbar").show(); me.down("statusbar").setStatus({ text: ' Certification Warning', }); me.down("statusbar").down("button").show(); } ); if (!eventsOnDom) { require("electron") .remote.webContents.fromId(webview.getWebContentsId()) .on("before-input-event", (event, input) => { if (input.type !== "keyDown") return; var modifiers = []; if (input.shift) modifiers.push("shift"); if (input.control) modifiers.push("control"); if (input.alt) modifiers.push("alt"); if (input.meta) modifiers.push("meta"); if (input.isAutoRepeat) modifiers.push("isAutoRepeat"); if (input.key === "Tab" && !(modifiers && modifiers.length)) return; // Maps special keys to fire the correct event in Mac OS if (require("electron").remote.process.platform === "darwin") { var keys = []; keys["ƒ"] = "f"; // Search keys[" "] = "l"; // Lock keys["∂"] = "d"; // DND input.key = keys[input.key] ? keys[input.key] : input.key; } if ( input.key === "F11" || input.key === "a" || input.key === "A" || input.key === "F12" || input.key === "q" || (input.key === "F1" && modifiers.includes("control")) ) return; require("electron").remote.getCurrentWebContents().sendInputEvent({ type: input.type, keyCode: input.key, modifiers: modifiers, }); }); eventsOnDom = true; Rambox.app.config.googleURLs.forEach((loginURL) => { if (webview.getURL().indexOf(loginURL) > -1) webview.reload(); }); } webview .executeJavaScript(js_inject) .then((result) => {}) .catch((err) => { console.log(err); }); }); webview.addEventListener("ipc-message", function (event) { var channel = event.channel; switch (channel) { case "rambox.setUnreadCount": handleSetUnreadCount(event); break; case "rambox.clearUnreadCount": handleClearUnreadCount(event); break; case "rambox.showWindowAndActivateTab": showWindowAndActivateTab(event); break; } /** * Handles 'rambox.clearUnreadCount' messages. * Clears the unread count. */ function handleClearUnreadCount() { me.tab.setBadgeText(""); me.currentUnreadCount = 0; me.setUnreadCount(0); } /** * Handles 'rambox.setUnreadCount' messages. * Sets the badge text if the event contains an integer or a '•' (indicating non-zero but unknown number of unreads) as first argument. * * @param event */ function handleSetUnreadCount(event) { if (Array.isArray(event.args) === true && event.args.length > 0) { var count = event.args[0]; if (count === parseInt(count, 10) || "•" === count) { if (count === 999999) count = "•"; me.setUnreadCount(count); } } } function showWindowAndActivateTab(event) { require("electron").remote.getCurrentWindow().show(); var tabPanel = Ext.cq1("app-main"); // Temp fix missing cursor after upgrade to electron 3.x + tabPanel.setActiveTab(me); tabPanel.getActiveTab().getWebView().blur(); tabPanel.getActiveTab().getWebView().focus(); } }); /** * Register page title update event listener only for services that don't specify a js_unread */ if ( Ext.getStore("ServicesList").getById(me.record.get("type")) ? Ext.getStore("ServicesList") .getById(me.record.get("type")) .get("js_unread") === "" : false && me.record.get("js_unread") === "" ) { webview.addEventListener("page-title-updated", function (e) { var count = e.title.match(/\(([^)]+)\)/); // Get text between (...) count = count ? count[1] : "0"; count = count === "•" ? count : Ext.isArray(count.match(/\d+/g)) ? count.match(/\d+/g).join("") : count.match(/\d+/g); // Some services have special characters. Example: (•) count = count === null ? "0" : count; me.setUnreadCount(count); }); } webview.addEventListener("did-navigate", function (e) { if (e.isMainFrame && me.record.get("type") === "tweetdeck") Ext.defer(function () { webview.loadURL(e.newURL); }, 1000); // Applied a defer because sometimes is not redirecting. TweetDeck 2FA is an example. }); webview.addEventListener("update-target-url", function (url) { me.down("statusbar #url").setText(url.url); }); }, setUnreadCount: function (newUnreadCount) { var me = this; if ( !isNaN(newUnreadCount) && (function (x) { return (x | 0) === x; })(parseFloat(newUnreadCount)) && me.record.get("includeInGlobalUnreadCounter") === true ) { Rambox.util.UnreadCounter.setUnreadCountForService( me.record.get("id"), newUnreadCount ); } else { Rambox.util.UnreadCounter.clearUnreadCountForService(me.record.get("id")); } me.setTabBadgeText(Rambox.util.Format.formatNumber(newUnreadCount)); me.doManualNotification(parseInt(newUnreadCount)); }, refreshUnreadCount: function () { this.setUnreadCount(this.currentUnreadCount); }, /** * Dispatch manual notification if * • service doesn't have notifications, so Rambox does them * • count increased * • not in dnd mode * • notifications enabled * * @param {int} count */ doManualNotification: function (count) { var me = this; var manualNotifications = Ext.getStore("ServicesList").getById(me.type) ? Ext.getStore("ServicesList") .getById(me.type) .get("manual_notifications") : false; if ( manualNotifications && me.currentUnreadCount < count && me.record.get("notifications") && !JSON.parse(localStorage.getItem("dontDisturb")) ) { Rambox.util.Notifier.dispatchNotification(me, count); } me.currentUnreadCount = count; }, /** * Sets the tab badge text depending on the service config param "displayTabUnreadCounter". * * @param {string} badgeText */ setTabBadgeText: function (badgeText) { var me = this; if (me.record.get("displayTabUnreadCounter") === true) { me.tab.setBadgeText(badgeText); } else { me.tab.setBadgeText(""); } }, /** * Clears the unread counter for this view: * • clears the badge text * • clears the global unread counter */ clearUnreadCounter: function () { var me = this; me.tab.setBadgeText(""); Rambox.util.UnreadCounter.clearUnreadCountForService(me.record.get("id")); }, reloadService: function (btn) { var me = this; var webview = me.getWebView(); if (me.record.get("enabled")) { me.clearUnreadCounter(); webview.loadURL(me.src); } }, onFailLoad: function (v) { let me = this; me.errorCodeLog = []; setTimeout( () => Ext.getCmp(me.id + "statusbar").setStatus({ text: ' The service failed at loading, Error: ' + v, }), 1000 ); }, showSearchBox: function (v) { var me = this; if (!me.record.get("enabled")) return; var webview = me.getWebView(); webview.stopFindInPage("keepSelection"); if (v) { me.down("#searchBar").show(); setTimeout(() => { me.down("#searchBar textfield").focus(); }, 100); } else { me.down("#searchBar").hide(); me.down("#searchBar textfield").setValue(""); } me.down("#searchBar displayfield").setValue(""); }, doSearchText: function (field, newValue, oldValue, eOpts, forward = true) { var me = this; var webview = me.getWebView(); if (newValue === "") { webview.stopFindInPage("clearSelection"); me.down("#searchBar displayfield").setValue(""); return; } webview.findInPage(newValue, { forward: forward, findNext: false, matchCase: false, }); }, onSearchText: function (result) { var me = this; me.down("#searchBar displayfield").setValue( result.activeMatchOrdinal + "/" + result.matches ); }, toggleDevTools: function (btn) { var me = this; var webview = me.getWebView(); if (me.record.get("enabled")) webview.isDevToolsOpened() ? webview.closeDevTools() : webview.openDevTools(); }, setURL: function (url) { var me = this; var webview = me.getWebView(); me.src = url; if (me.record.get("enabled")) webview.loadURL(url); }, setAudioMuted: function (muted, calledFromDisturb) { var me = this; var webview = me.getWebView(); me.muted = muted; if ( !muted && !calledFromDisturb && JSON.parse(localStorage.getItem("dontDisturb")) ) return; if (me.record.get("enabled")) webview.setAudioMuted(muted); }, closeStatusBar: function () { var me = this; me.down("statusbar").hide(); me.down("statusbar").closed = true; me.down("statusbar").keep = me.record.get("statusbar"); }, setStatusBar: function (keep) { var me = this; me.removeDocked(me.down("statusbar"), true); if (keep) { me.addDocked(me.statusBarConstructor(false)); } else { me.add(me.statusBarConstructor(true)); } me.down("statusbar").keep = keep; }, setNotifications: function (notification, calledFromDisturb) { var me = this; var webview = me.getWebView(); me.notifications = notification; if ( notification && !calledFromDisturb && JSON.parse(localStorage.getItem("dontDisturb")) ) return; if (me.record.get("enabled")) ipc.send("setServiceNotifications", webview.partition, notification); }, setEnabled: function (enabled) { var me = this; me.clearUnreadCounter(); me.removeAll(); me.add(me.webViewConstructor(enabled)); if (enabled) { me.resumeEvent("afterrender"); me.show(); me.tab.setStyle("-webkit-filter", "grayscale(0)"); me.onAfterRender(); } else { me.suspendEvent("afterrender"); me.tab.setStyle("-webkit-filter", "grayscale(1)"); } }, goBack: function () { var me = this; var webview = me.getWebView(); if (me.record.get("enabled")) webview.goBack(); }, goForward: function () { var me = this; var webview = me.getWebView(); if (me.record.get("enabled")) webview.goForward(); }, zoomIn: function () { if (this.timeout) clearTimeout(this.timeout); this.timeout = setTimeout(() => { var me = this; var webview = me.getWebView(); me.zoomLevel = me.zoomLevel + 0.25; if (me.record.get("enabled")) { webview.setZoomLevel(me.zoomLevel); me.record.set("zoomLevel", me.zoomLevel); } }, 100); }, zoomOut: function () { if (this.timeout) clearTimeout(this.timeout); this.timeout = setTimeout(() => { var me = this; var webview = me.getWebView(); me.zoomLevel = me.zoomLevel - 0.25; if (me.record.get("enabled")) { webview.setZoomLevel(me.zoomLevel); me.record.set("zoomLevel", me.zoomLevel); } }, 100); }, resetZoom: function () { var me = this; var webview = me.getWebView(); me.zoomLevel = 0; if (me.record.get("enabled")) { webview.setZoomLevel(0); me.record.set("zoomLevel", me.zoomLevel); } }, getWebView: function () { if (this.record.get("enabled")) { return this.down("component[cls=webview]").el.dom; } else { return false; } }, });