diff --git a/src/app/atoms/avatar/render.js b/src/app/atoms/avatar/render.js index 07303dd1..e8cf1a66 100644 --- a/src/app/atoms/avatar/render.js +++ b/src/app/atoms/avatar/render.js @@ -1,8 +1,4 @@ -import { avatarInitials } from '../../../util/common'; - -function cssVar(name) { - return getComputedStyle(document.body).getPropertyValue(name); -} +import { avatarInitials, cssVar } from '../../../util/common'; // renders the avatar and returns it as an URL export default async function renderAvatar({ diff --git a/src/client/initMatrix.js b/src/client/initMatrix.js index e219a777..7e028f61 100644 --- a/src/client/initMatrix.js +++ b/src/client/initMatrix.js @@ -67,7 +67,7 @@ class InitMatrix extends EventEmitter { }, PREPARED: (prevState) => { console.log('PREPARED state'); - console.log('previous state: ', prevState); + console.log('Previous state: ', prevState); // TODO: remove global.initMatrix at end global.initMatrix = this; if (prevState === null) { @@ -76,6 +76,8 @@ class InitMatrix extends EventEmitter { this.roomsInput = new RoomsInput(this.matrixClient, this.roomList); this.notifications = new Notifications(this.roomList); this.emit('init_loading_finished'); + } else { + this.notifications._initNoti(); } }, RECONNECTING: () => { diff --git a/src/client/state/Notifications.js b/src/client/state/Notifications.js index 4be7a112..3a78d44e 100644 --- a/src/client/state/Notifications.js +++ b/src/client/state/Notifications.js @@ -5,6 +5,9 @@ import { selectRoom } from '../action/navigation'; import cons from './cons'; import navigation from './navigation'; import settings from './settings'; +import { getBadgedFavicon, setFavicon, cssVar } from '../../util/common'; + +import LogoSVG from '../../../public/res/svg/cinny.svg'; function isNotifEvent(mEvent) { const eType = mEvent.getType(); @@ -32,22 +35,25 @@ class Notifications extends EventEmitter { constructor(roomList) { super(); + this.LOGO_HIGHLIGH = LogoSVG; + this.LOGO_UNREAD = LogoSVG; + this.matrixClient = roomList.matrixClient; this.roomList = roomList; this.roomIdToNoti = new Map(); - this._initNoti(); + // this._initNoti(); this._listenEvents(); // Ask for permission by default after loading window.Notification?.requestPermission(); - - // TODO: - window.notifications = this; } - _initNoti() { + async _initNoti() { + this.LOGO_HIGHLIGH = await getBadgedFavicon(LogoSVG, cssVar('--bg-positive')) ?? LogoSVG; + this.LOGO_UNREAD = await getBadgedFavicon(LogoSVG, cssVar('--bg-badge')) ?? LogoSVG; + const addNoti = (roomId) => { const room = this.matrixClient.getRoom(roomId); if (this.getNotiType(room.roomId) === cons.notifs.MUTE) return; @@ -59,6 +65,7 @@ class Notifications extends EventEmitter { }; [...this.roomList.rooms].forEach(addNoti); [...this.roomList.directs].forEach(addNoti); + this._updateFavicon(); } doesRoomHaveUnread(room) { @@ -130,6 +137,24 @@ class Notifications extends EventEmitter { } } + async _updateFavicon() { + let unread = false; + let highlight = false; + [...this.roomIdToNoti.values()].find((noti) => { + if (!unread) { + unread = noti.total > 0 || noti.highlight > 0; + } + highlight = noti.highlight > 0; + if (unread && highlight) return true; + return false; + }); + if (!unread) { + setFavicon(LogoSVG); + return; + } + setFavicon(highlight ? this.LOGO_HIGHLIGH : this.LOGO_UNREAD); + } + _setNoti(roomId, total, highlight) { const addNoti = (id, t, h, fromId) => { const prevTotal = this.roomIdToNoti.get(id)?.total ?? null; @@ -156,6 +181,7 @@ class Notifications extends EventEmitter { allParentSpaces.forEach((spaceId) => { addNoti(spaceId, addT, addH, roomId); }); + this._updateFavicon(); } _deleteNoti(roomId, total, highlight) { @@ -188,6 +214,7 @@ class Notifications extends EventEmitter { allParentSpaces.forEach((spaceId) => { removeNoti(spaceId, total, highlight, roomId); }); + this._updateFavicon(); } async _displayPopupNoti(mEvent, room) { diff --git a/src/util/common.js b/src/util/common.js index 83fd20fe..c5cdd05a 100644 --- a/src/util/common.js +++ b/src/util/common.js @@ -115,6 +115,46 @@ export function avatarInitials(text) { return [...text][0]; } +export function cssVar(name) { + return getComputedStyle(document.body).getPropertyValue(name); +} + +export function setFavicon(url) { + document.querySelector('[rel=icon]').href = url; +} + +export async function getBadgedFavicon(favUrl, color) { + try { + const canvas = document.createElement('canvas'); + canvas.width = 48; + canvas.height = 48; + + const ctx = canvas.getContext('2d'); + + const img = new Image(); + img.crossOrigin = 'anonymous'; + const imgPromise = new Promise((resolve, reject) => { + img.onerror = reject; + img.onload = resolve; + }); + img.src = favUrl; + await imgPromise; + + ctx.drawImage(img, 0, 0, 48, 48); + + ctx.beginPath(); + ctx.fillStyle = color; + ctx.arc(40, 8, 8, 0, 2 * Math.PI); + ctx.closePath(); + ctx.fill(); + + return canvas.toDataURL(); + } catch (e) { + console.error(e); + return undefined; + } +} + export function copyToClipboard(text) { if (navigator.clipboard) { navigator.clipboard.writeText(text);