Replace favico.js dependency with simplified variant grown from it

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
Michael Telatynski 2020-05-13 05:19:08 +01:00
parent d44808e0e6
commit 911bd2e469
5 changed files with 266 additions and 30 deletions

View file

@ -58,7 +58,6 @@
},
"dependencies": {
"browser-request": "^0.3.3",
"favico.js": "^0.3.10",
"gfm.css": "^1.1.2",
"highlight.js": "^9.13.1",
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop",

View file

@ -24,5 +24,11 @@ declare global {
// electron-only
ipcRenderer: any;
// opera-only
opera: any;
// https://developer.mozilla.org/en-US/docs/Web/API/InstallTrigger
InstallTrigger: any;
}
}

256
src/favicon.ts Normal file
View file

@ -0,0 +1,256 @@
/*
Copyright 2020 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Based upon https://github.com/ejci/favico.js/blob/master/favico.js [MIT license]
interface IParams {
// colour parameters
bgColor: string;
textColor: string;
// font styling parameters
fontFamily: string;
fontWeight: "normal" | "italic" | "bold" | "bolder" | "lighter" | number;
// positioning parameters
isUp: boolean;
isLeft: boolean;
}
const defaults: IParams = {
bgColor: "#d00",
textColor: "#fff",
fontFamily: "sans-serif", // Arial,Verdana,Times New Roman,serif,sans-serif,...
fontWeight: "bold", // normal,italic,oblique,bold,bolder,lighter,100,200,300,400,500,600,700,800,900
isUp: false,
isLeft: false,
};
// Allows dynamic rendering of a circular badge atop the loaded favicon
// supports colour, font and basic positioning parameters.
export default class Favicon {
private readonly browser = {
ff: typeof window.InstallTrigger !== "undefined",
opera: !!window.opera || navigator.userAgent.includes("Opera"),
};
private readonly params: IParams;
private readonly canvas: HTMLCanvasElement;
private readonly baseImage: HTMLImageElement;
private context: CanvasRenderingContext2D;
private icons: HTMLLinkElement[];
private isReady: boolean = false;
// callback to run once isReady is asserted, allows for a badge to be queued for when it can be shown
private readyCb = () => {};
constructor(params: Partial<IParams> = {}) {
this.params = {...defaults, ...params};
this.icons = Favicon.getIcons();
// create work canvas
this.canvas = document.createElement("canvas");
// create clone of favicon as a base
this.baseImage = document.createElement("img");
const lastIcon = this.icons[this.icons.length - 1];
if (lastIcon.hasAttribute("href")) {
this.baseImage.setAttribute("crossOrigin", "anonymous");
this.baseImage.onload = () => {
// get height and width of the favicon
this.canvas.height = (this.baseImage.height > 0) ? this.baseImage.height : 32;
this.canvas.width = (this.baseImage.width > 0) ? this.baseImage.width : 32;
this.context = this.canvas.getContext("2d");
this.ready();
};
this.baseImage.setAttribute("src", lastIcon.getAttribute("href"));
} else {
this.canvas.height = this.baseImage.height = 32;
this.canvas.width = this.baseImage.width = 32;
this.context = this.canvas.getContext("2d");
this.ready();
}
}
private reset() {
this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
this.context.drawImage(this.baseImage, 0, 0, this.canvas.width, this.canvas.height);
}
private options(n: number | string) {
const opt = {
n: ((typeof n) === "number") ? Math.abs(n as number | 0) : n,
len: ("" + n).length,
// badge positioning constants as percentages
x: 0.4,
y: 0.4,
w: 0.6,
h: 0.6,
};
// apply positional transformations
if (this.params.isUp) {
if (opt.y < 0.6) {
opt.y = opt.y - 0.4;
} else {
opt.y = opt.y - 2 * opt.y + (1 - opt.w);
}
}
if (this.params.isLeft) {
if (opt.x < 0.6) {
opt.x = opt.x - 0.4;
} else {
opt.x = opt.x - 2 * opt.x + (1 - opt.h);
}
}
// scale the position to the canvas
opt.x = this.canvas.width * opt.x;
opt.y = this.canvas.height * opt.y;
opt.w = this.canvas.width * opt.w;
opt.h = this.canvas.height * opt.h;
return opt;
}
private circle(n: number | string) {
const opt = this.options(n);
let more = false;
if (opt.len === 2) {
opt.x = opt.x - opt.w * 0.4;
opt.w = opt.w * 1.4;
more = true;
} else if (opt.len >= 3) {
opt.x = opt.x - opt.w * 0.65;
opt.w = opt.w * 1.65;
more = true;
}
this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
this.context.drawImage(this.baseImage, 0, 0, this.canvas.width, this.canvas.height);
this.context.beginPath();
const fontSize = Math.floor(opt.h * (opt.n > 99 ? 0.85 : 1)) + "px";
this.context.font = `${this.params.fontWeight} ${fontSize} ${this.params.fontFamily}`;
this.context.textAlign = "center";
if (more) {
this.context.moveTo(opt.x + opt.w / 2, opt.y);
this.context.lineTo(opt.x + opt.w - opt.h / 2, opt.y);
this.context.quadraticCurveTo(opt.x + opt.w, opt.y, opt.x + opt.w, opt.y + opt.h / 2);
this.context.lineTo(opt.x + opt.w, opt.y + opt.h - opt.h / 2);
this.context.quadraticCurveTo(opt.x + opt.w, opt.y + opt.h, opt.x + opt.w - opt.h / 2, opt.y + opt.h);
this.context.lineTo(opt.x + opt.h / 2, opt.y + opt.h);
this.context.quadraticCurveTo(opt.x, opt.y + opt.h, opt.x, opt.y + opt.h - opt.h / 2);
this.context.lineTo(opt.x, opt.y + opt.h / 2);
this.context.quadraticCurveTo(opt.x, opt.y, opt.x + opt.h / 2, opt.y);
} else {
this.context.arc(opt.x + opt.w / 2, opt.y + opt.h / 2, opt.h / 2, 0, 2 * Math.PI);
}
this.context.fillStyle = this.params.bgColor;
this.context.fill();
this.context.closePath();
this.context.beginPath();
this.context.stroke();
this.context.fillStyle = this.params.textColor;
if ((typeof opt.n) === "number" && opt.n > 999) {
const count = ((opt.n > 9999) ? 9 : Math.floor(opt.n as number / 1000)) + "k+";
this.context.fillText(count, Math.floor(opt.x + opt.w / 2), Math.floor(opt.y + opt.h - opt.h * 0.2));
} else {
this.context.fillText("" + opt.n, Math.floor(opt.x + opt.w / 2), Math.floor(opt.y + opt.h - opt.h * 0.15));
}
this.context.closePath();
}
private ready() {
if (this.isReady) return;
this.isReady = true;
this.readyCb();
}
private setIcon(canvas) {
setImmediate(() => {
this.setIconSrc(canvas.toDataURL("image/png"));
});
}
private setIconSrc(url) {
// if is attached to fav icon
if (this.browser.ff || this.browser.opera) {
// for FF we need to "recreate" element, attach to dom and remove old <link>
const old = this.icons[this.icons.length - 1];
const newIcon = window.document.createElement("link");
this.icons = [newIcon];
newIcon.setAttribute("rel", "icon");
newIcon.setAttribute("type", "image/png");
window.document.getElementsByTagName("head")[0].appendChild(newIcon);
newIcon.setAttribute("href", url);
if (old.parentNode) {
old.parentNode.removeChild(old);
}
} else {
this.icons.forEach(icon => {
icon.setAttribute("href", url);
});
}
}
public badge(content: number | string) {
if (!this.isReady) {
this.readyCb = () => {
this.badge(content);
}
return;
}
if (typeof content === "string" || content > 0) {
this.circle(content);
} else {
this.reset();
}
this.setIcon(this.canvas);
}
private static getLinks() {
const icons: HTMLLinkElement[] = [];
const links = window.document.getElementsByTagName("head")[0].getElementsByTagName("link");
for (let i = 0; i < links.length; i++) {
if ((/(^|\s)icon(\s|$)/i).test(links[i].getAttribute("rel"))) {
icons.push(links[i]);
}
}
return icons;
}
private static getIcons() {
// get favicon link elements
let elms = Favicon.getLinks();
// if link element
if (elms.length === 0) {
elms = [window.document.createElement("link")];
elms[0].setAttribute("rel", "icon");
window.document.getElementsByTagName("head")[0].appendChild(elms[0]);
}
elms.forEach(item => {
item.setAttribute("type", "image/png");
});
return elms;
}
}

View file

@ -24,7 +24,7 @@ import { _t } from 'matrix-react-sdk/src/languageHandler';
import dis from 'matrix-react-sdk/src/dispatcher';
import {getVectorConfig} from "../getconfig";
import Favico from 'favico.js';
import Favico from '../../favicon';
export const updateCheckStatusEnum = {
CHECKING: 'CHECKING',
@ -85,29 +85,9 @@ export default class VectorBasePlatform extends BasePlatform {
bgColor = "#f00";
}
const doUpdate = () => {
this.favicon.badge(notif, {
bgColor: bgColor,
});
};
doUpdate();
// HACK: Workaround for Chrome 78+ and dependency incompatibility.
// The library we use doesn't appear to work in Chrome 78, likely due to their
// changes surrounding tab behaviour. Tabs went through a bit of a redesign and
// restructuring in Chrome 78, so it's not terribly surprising that the library
// doesn't work correctly. The library we use hasn't been updated in years and
// does not look easy to fix/fork ourselves - we might as well write our own that
// doesn't include animation/webcam/etc support. However, that's a bit difficult
// so for now we'll just trigger the update twice.
//
// Note that trying to reproduce the problem in isolation doesn't seem to work:
// see https://gist.github.com/turt2live/5ab87919918adbfd7cfb8f1ad10f2409 for
// an example (you'll need your own web server to host that).
if (window.chrome) {
doUpdate();
}
} catch (e) {
console.warn(`Failed to set badge count: ${e.message}`);
}

View file

@ -4284,11 +4284,6 @@ fast-levenshtein@~2.0.6:
resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=
favico.js@^0.3.10:
version "0.3.10"
resolved "https://registry.yarnpkg.com/favico.js/-/favico.js-0.3.10.tgz#80586e27a117f24a8d51c18a99bdc714d4339301"
integrity sha1-gFhuJ6EX8kqNUcGKmb3HFNQzkwE=
faye-websocket@^0.10.0:
version "0.10.0"
resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.10.0.tgz#4e492f8d04dfb6f89003507f6edbf2d501e7c6f4"