Create icons for users without an avatar in desktop notifications (#305)
authorginnyTheCat <ginnythecat@lelux.net>
Tue, 15 Feb 2022 11:48:25 +0000 (12:48 +0100)
committerGitHub <noreply@github.com>
Tue, 15 Feb 2022 11:48:25 +0000 (17:18 +0530)
* Add notifications icon for users without an avatar

* Render icon at higher resolution

* Use scale to render at higher resolution

src/app/atoms/avatar/Avatar.jsx
src/app/atoms/avatar/render.js [new file with mode: 0644]
src/client/state/Notifications.js
src/util/colorMXID.js
src/util/common.js

index 891637c9066ebce02f9b20a7ac0150a911fc5cd3..6120e55111f58cb9a119a3d73244c2cb6f671f87 100644 (file)
@@ -8,6 +8,7 @@ import Text from '../text/Text';
 import RawIcon from '../system-icons/RawIcon';
 
 import ImageBrokenSVG from '../../../../public/res/svg/image-broken.svg';
+import { avatarInitials } from '../../../util/common';
 
 function Avatar({
   text, bgColor, iconSrc, iconColor, imageSrc, size,
@@ -40,7 +41,7 @@ function Avatar({
                   ? <RawIcon size={size} src={iconSrc} color={iconColor} />
                   : text !== null && (
                     <Text variant={textSize} primary>
-                      {twemojify([...text][0])}
+                      {twemojify(avatarInitials(text))}
                     </Text>
                   )
               }
diff --git a/src/app/atoms/avatar/render.js b/src/app/atoms/avatar/render.js
new file mode 100644 (file)
index 0000000..07303dd
--- /dev/null
@@ -0,0 +1,61 @@
+import { avatarInitials } from '../../../util/common';
+
+function cssVar(name) {
+  return getComputedStyle(document.body).getPropertyValue(name);
+}
+
+// renders the avatar and returns it as an URL
+export default async function renderAvatar({
+  text, bgColor, imageSrc, size, borderRadius, scale,
+}) {
+  try {
+    const canvas = document.createElement('canvas');
+    canvas.width = size * scale;
+    canvas.height = size * scale;
+
+    const ctx = canvas.getContext('2d');
+
+    ctx.scale(scale, scale);
+
+    // rounded corners
+    ctx.beginPath();
+    ctx.moveTo(size, size);
+    ctx.arcTo(0, size, 0, 0, borderRadius);
+    ctx.arcTo(0, 0, size, 0, borderRadius);
+    ctx.arcTo(size, 0, size, size, borderRadius);
+    ctx.arcTo(size, size, 0, size, borderRadius);
+
+    if (imageSrc) {
+      // clip corners of image
+      ctx.closePath();
+      ctx.clip();
+
+      const img = new Image();
+      img.crossOrigin = 'anonymous';
+      const promise = new Promise((resolve, reject) => {
+        img.onerror = reject;
+        img.onload = resolve;
+      });
+      img.src = imageSrc;
+      await promise;
+
+      ctx.drawImage(img, 0, 0, size, size);
+    } else {
+      // colored background
+      ctx.fillStyle = cssVar(bgColor);
+      ctx.fill();
+
+      // centered letter
+      ctx.fillStyle = '#fff';
+      ctx.font = `${cssVar('--fs-s1')} ${cssVar('--font-primary')}`;
+      ctx.textBaseline = 'middle';
+      ctx.textAlign = 'center';
+      ctx.fillText(avatarInitials(text), size / 2, size / 2);
+    }
+
+    return canvas.toDataURL();
+  } catch (e) {
+    console.error(e);
+    return imageSrc;
+  }
+}
index 94626649f50d243fecb9022231413edf26fdd5cf..a41633ba05c94c44d7b960b5aca0f71621e61cd8 100644 (file)
@@ -1,4 +1,6 @@
 import EventEmitter from 'events';
+import renderAvatar from '../../app/atoms/avatar/render';
+import { cssColorMXID } from '../../util/colorMXID';
 import { selectRoom } from '../action/navigation';
 import cons from './cons';
 import navigation from './navigation';
@@ -183,9 +185,19 @@ class Notifications extends EventEmitter {
       title = `${mEvent.sender.name} (${room.name})`;
     }
 
+    const iconSize = 36;
+    const icon = await renderAvatar({
+      text: mEvent.sender.name,
+      bgColor: cssColorMXID(mEvent.getSender()),
+      imageSrc: mEvent.sender?.getAvatarUrl(this.matrixClient.baseUrl, iconSize, iconSize, 'crop'),
+      size: iconSize,
+      borderRadius: 8,
+      scale: 8,
+    });
+
     const noti = new window.Notification(title, {
       body: mEvent.getContent().body,
-      icon: mEvent.sender?.getAvatarUrl(this.matrixClient.baseUrl, 36, 36, 'crop'),
+      icon,
     });
     noti.onclick = () => selectRoom(room.roomId, mEvent.getId());
   }
index 4745120c95c75297c6a733cd92e934b66fda4b08..4d303aae4b07f200a23bc8b790c43b2122daa827 100644 (file)
@@ -1,16 +1,6 @@
 // https://github.com/cloudrac3r/cadencegq/blob/master/pug/mxid.pug
 
-const colors = [
-  'var(--mx-uc-1)',
-  'var(--mx-uc-2)',
-  'var(--mx-uc-3)',
-  'var(--mx-uc-4)',
-  'var(--mx-uc-5)',
-  'var(--mx-uc-6)',
-  'var(--mx-uc-7)',
-  'var(--mx-uc-8)',
-];
-function hashCode(str) {
+export function hashCode(str) {
   let hash = 0;
   let i;
   let chr;
@@ -26,7 +16,12 @@ function hashCode(str) {
   }
   return Math.abs(hash);
 }
-export default function colorMXID(userId) {
+
+export function cssColorMXID(userId) {
   const colorNumber = hashCode(userId) % 8;
-  return colors[colorNumber];
+  return `--mx-uc-${colorNumber + 1}`;
+}
+
+export default function colorMXID(userId) {
+  return `var(${cssColorMXID(userId)})`;
 }
index 914242088ceb990f1ab8674971448c4564e0414d..941f34cfe533df2ec7907715a2393e6924f8357c 100644 (file)
@@ -110,3 +110,7 @@ export function getScrollInfo(target) {
   scroll.isScrollable = scroll.height > scroll.viewHeight;
   return scroll;
 }
+
+export function avatarInitials(text) {
+  return [...text][0];
+}