diff --git a/app/soapbox/components/helmet.js b/app/soapbox/components/helmet.js index 8607ad4d4..a677ce2bf 100644 --- a/app/soapbox/components/helmet.js +++ b/app/soapbox/components/helmet.js @@ -5,6 +5,7 @@ import { withRouter } from 'react-router-dom'; import { Helmet } from'react-helmet'; import { getSettings } from 'soapbox/actions/settings'; import sourceCode from 'soapbox/utils/code'; +import FaviconService from 'soapbox/utils/favicon_service'; const getNotifTotals = state => { const notifications = state.getIn(['notifications', 'unread'], 0); @@ -35,8 +36,15 @@ class SoapboxHelmet extends React.Component { addCounter = title => { const { unreadCount, demetricator } = this.props; - if (unreadCount < 1 || demetricator) return title; - return `(${unreadCount}) ${title}`; + + if (unreadCount < 1 || demetricator) { + // Erase badge when there are no notifications + FaviconService.clearFaviconBadge(); + return title; + } else { + FaviconService.drawFaviconBadge(); + return `(${unreadCount}) ${title}`; + } } render() { diff --git a/app/soapbox/main.js b/app/soapbox/main.js index 91822efef..95e1b13fc 100644 --- a/app/soapbox/main.js +++ b/app/soapbox/main.js @@ -12,6 +12,7 @@ import * as perf from './performance'; import * as monitoring from './monitoring'; import ready from './ready'; import { NODE_ENV } from 'soapbox/build_config'; +import FaviconService from 'soapbox/utils/favicon_service'; function main() { perf.start('main()'); @@ -19,6 +20,9 @@ function main() { // Sentry monitoring.start(); + // Favicon unread badge + FaviconService.initFaviconService(); + ready(() => { const mountNode = document.getElementById('soapbox'); diff --git a/app/soapbox/utils/favicon_service.js b/app/soapbox/utils/favicon_service.js new file mode 100644 index 000000000..9e14aad96 --- /dev/null +++ b/app/soapbox/utils/favicon_service.js @@ -0,0 +1,69 @@ +// Adapted from Pleroma FE +// https://git.pleroma.social/pleroma/pleroma-fe/-/blob/ef5bbc4e5f84bb9e8da76a0440eea5d656d36977/src/services/favicon_service/favicon_service.js + +const createFaviconService = () => { + const favicons = []; + const faviconWidth = 128; + const faviconHeight = 128; + const badgeRadius = 32; + + const initFaviconService = () => { + const nodes = document.querySelectorAll('link[rel="icon"]'); + nodes.forEach(favicon => { + if (favicon) { + const favcanvas = document.createElement('canvas'); + favcanvas.width = faviconWidth; + favcanvas.height = faviconHeight; + const favimg = new Image(); + favimg.crossOrigin = 'anonymous'; + favimg.src = favicon.href; + const favcontext = favcanvas.getContext('2d'); + favicons.push({ favcanvas, favimg, favcontext, favicon }); + } + }); + }; + + const isImageLoaded = (img) => img.complete && img.naturalHeight !== 0; + + const clearFaviconBadge = () => { + if (favicons.length === 0) return; + favicons.forEach(({ favimg, favcanvas, favcontext, favicon }) => { + if (!favimg || !favcontext || !favicon) return; + favcontext.clearRect(0, 0, faviconWidth, faviconHeight); + if (isImageLoaded(favimg)) { + favcontext.drawImage(favimg, 0, 0, favimg.width, favimg.height, 0, 0, faviconWidth, faviconHeight); + } + favicon.href = favcanvas.toDataURL('image/png'); + }); + }; + + const drawFaviconBadge = () => { + if (favicons.length === 0) return; + clearFaviconBadge(); + favicons.forEach(({ favimg, favcanvas, favcontext, favicon }) => { + if (!favimg || !favcontext || !favcontext) return; + + const style = getComputedStyle(document.body); + const badgeColor = `${style.getPropertyValue('--badgeNotification') || 'rgb(240, 100, 100)'}`; + + if (isImageLoaded(favimg)) { + favcontext.drawImage(favimg, 0, 0, favimg.width, favimg.height, 0, 0, faviconWidth, faviconHeight); + } + favcontext.fillStyle = badgeColor; + favcontext.beginPath(); + favcontext.arc(faviconWidth - badgeRadius, badgeRadius, badgeRadius, 0, 2 * Math.PI, false); + favcontext.fill(); + favicon.href = favcanvas.toDataURL('image/png'); + }); + }; + + return { + initFaviconService, + clearFaviconBadge, + drawFaviconBadge, + }; +}; + +const FaviconService = createFaviconService(); + +export default FaviconService;