Форк Rambox
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

979 lines
28 KiB

/**
* 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: "<h3>Service Disabled</h3>",
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: '<i class="fa fa-check fa-fw" aria-hidden="true"></i> Ready',
busyIconCls: "",
busyText:
'<i class="fa fa-circle-o-notch fa-spin fa-fw"></i> ' +
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:
'<i class="fa fa-exclamation-triangle" aria-hidden="true"></i> 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:
'<i class="fa fa-warning fa-fw" aria-hidden="true"></i> 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;
}
},
});