فهرست منبع

XEP-0392 implementation: colorize nicknames

John Livingston 1 سال پیش
والد
کامیت
a1d28391f0

+ 1 - 0
CHANGES.md

@@ -3,6 +3,7 @@
 ## 11.0.0 (Unreleased)
 
 - #1195: Add actions to quote and copy messages
+- #1349: New config option [colorize_username](https://conversejs.org/docs/html/configuration.html#colorize_username)
 - #2716: Fix issue with chat display when opening via URL
 - #3033: Add the `muc_grouped_by_domain` option to display MUCs on the same domain in collapsible groups
 - #3155: Some ad-hoc commands not working

+ 6 - 0
docs/source/configuration.rst

@@ -2184,6 +2184,12 @@ theme
 
 Let's you set a color theme for Converse.
 
+colorize_username
+-------------------
+
+* Default: ``false``
+
+Wether nicknames should be colorized, in compliance with `XEP-0392: Consistent Color Generation <https://xmpp.org/extensions/xep-0392.html>`_.
 
 time_format
 -----------

+ 6 - 0
package-lock.json

@@ -19,6 +19,7 @@
         "dompurify": "^3.0.8",
         "favico.js-slevomat": "^0.3.11",
         "gifuct-js": "^2.1.2",
+        "hsluv": "^1.0.1",
         "jed": "1.1.1",
         "lit": "^2.4.0",
         "localforage-webextensionstorage-driver": "^3.0.0",
@@ -5887,6 +5888,11 @@
         "safe-buffer": "~5.1.0"
       }
     },
+    "node_modules/hsluv": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/hsluv/-/hsluv-1.0.1.tgz",
+      "integrity": "sha512-zCaFTiDqBLQjCCFBu0qg7z9ASYPd+Bxx2GDCVZJsnehjK80S+jByqhuFz0pCd2Aw3FSKr18AWbRlwnKR0YdizQ=="
+    },
     "node_modules/html-encoding-sniffer": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz",

+ 1 - 0
package.json

@@ -124,6 +124,7 @@
     "dompurify": "^3.0.8",
     "favico.js-slevomat": "^0.3.11",
     "gifuct-js": "^2.1.2",
+    "hsluv": "^1.0.1",
     "jed": "1.1.1",
     "lit": "^2.4.0",
     "localforage-webextensionstorage-driver": "^3.0.0",

+ 3 - 0
src/headless/plugins/muc/occupant.js

@@ -78,6 +78,9 @@ class MUCOccupant extends Model {
         }
     }
 
+    getColor () {
+        return this.get('color') || '';
+    }
 
     isMember () {
         return ['admin', 'owner', 'member'].includes(this.get('affiliation'));

+ 10 - 0
src/headless/plugins/muc/occupants.js

@@ -11,6 +11,7 @@ import { Strophe } from 'strophe.js';
 import { getAffiliationList } from './affiliations/utils.js';
 import { occupantsComparator } from './utils.js';
 import { getUniqueId } from '../../utils/index.js';
+import { colorize } from '../../utils/color';
 
 const { u } = converse.env;
 
@@ -36,6 +37,15 @@ class MUCOccupants extends Collection {
 
     initialize() {
         this.on('change:nick', () => this.sort());
+        if (api.settings.get('colorize_username')) {
+            const updateColor = async (occupant) => {
+                const color = await colorize(occupant.get('nick'));
+                occupant.save('color', color);
+            };
+            this.on('add', updateColor);
+            this.on('change:nick', updateColor);
+        }
+
         this.on('change:role', () => this.sort());
     }
 

+ 1 - 0
src/headless/plugins/muc/plugin.js

@@ -87,6 +87,7 @@ converse.plugins.add('converse-muc', {
             'auto_join_on_invite': false,
             'auto_join_rooms': [],
             'auto_register_muc_nickname': false,
+            'colorize_username': false,
             'hide_muc_participants': false,
             'locked_muc_domain': false,
             'modtools_disable_assign': false,

+ 0 - 35
src/headless/types/plugins/muc/occupant.d.ts

@@ -1,35 +0,0 @@
-export default MUCOccupant;
-/**
- * Represents a participant in a MUC
- * @class
- * @namespace _converse.MUCOccupant
- * @memberOf _converse
- */
-declare class MUCOccupant extends Model {
-    constructor(attributes: any, options: any);
-    vcard: any;
-    defaults(): {
-        hats: any[];
-        show: string;
-        states: any[];
-    };
-    save(key: any, val: any, options: any): any;
-    getDisplayName(): any;
-    /**
-     * Return roles which may be assigned to this occupant
-     * @returns {typeof ROLES} - An array of assignable roles
-     */
-    getAssignableRoles(): typeof ROLES;
-    /**
-    * Return affiliations which may be assigned by this occupant
-    * @returns {typeof AFFILIATIONS} An array of assignable affiliations
-    */
-    getAssignableAffiliations(): typeof AFFILIATIONS;
-    isMember(): boolean;
-    isModerator(): boolean;
-    isSelf(): any;
-}
-import { Model } from "@converse/skeletor";
-import { ROLES } from "./constants.js";
-import { AFFILIATIONS } from "./constants.js";
-//# sourceMappingURL=occupant.d.ts.map

+ 9 - 0
src/headless/types/utils/color.d.ts

@@ -0,0 +1,9 @@
+/**
+ * Computes an RGB color as specified in XEP-0392
+ * https://xmpp.org/extensions/xep-0392.html
+ *
+ * @param {string} s JID or nickname to colorize
+ * @returns {Promise<string>}
+ */
+export function colorize(s: string): Promise<string>;
+//# sourceMappingURL=color.d.ts.map

+ 40 - 0
src/headless/utils/color.js

@@ -0,0 +1,40 @@
+import { Hsluv } from 'hsluv';
+
+const cache = new Map()
+
+/**
+ * Computes an RGB color as specified in XEP-0392
+ * https://xmpp.org/extensions/xep-0392.html
+ *
+ * @param {string} s JID or nickname to colorize
+ * @returns {Promise<string>}
+ */
+export async function colorize (s) {
+  // We cache results in `cache`, to avoid unecessary computing (as it can be called very often)
+  const v = cache.get(s);
+  if (v) return v;
+
+  // Run the input through SHA-1
+  const digest = Array.from(
+    new Uint8Array(
+      await crypto.subtle.digest('SHA-1',
+        new TextEncoder().encode(s)
+      )
+    )
+  );
+
+  // Treat the output as little endian and extract the least-significant 16 bits.
+  // (These are the first two bytes of the output, with the second byte being the most significant one.)
+  // Divide the value by 65536 (use float division) and multiply it by 360 (to map it to degrees in a full circle).
+  const angle = ((digest[0] + (digest[1] * 256)) / 65536.0) * 360;
+
+  // Convert HSLuv angle to RGB Hex notation
+  const hsluv = new Hsluv();
+  hsluv.hsluv_h = angle;
+  hsluv.hsluv_s = 100;
+  hsluv.hsluv_l = 50;
+  hsluv.hsluvToHex();
+
+  cache.set(s, hsluv.hex);
+  return hsluv.hex;
+}

+ 4 - 1
src/plugins/muc-views/templates/occupant.js

@@ -46,6 +46,9 @@ export default (o, chat) => {
         [classes, color] = ['fa fa-circle', 'subdued-color'];
     }
 
+    const occupant_color = o.getColor();
+    const occupant_style = color ? 'color: ' + occupant_color + ' !important;' : '';
+
     return html`
         <li class="occupant" id="${o.id}" title="${occupant_title(o)}">
             <div class="row no-gutters">
@@ -65,7 +68,7 @@ export default (o, chat) => {
                     </a>
                 </div>
                 <div class="col occupant-nick-badge">
-                    <span class="occupant-nick" @click=${chat.onOccupantClicked}>${o.getDisplayName()}</span>
+                    <span class="occupant-nick" @click=${chat.onOccupantClicked} style="${occupant_style}">${o.getDisplayName()}</span>
                     <span class="occupant-badges">
                         ${ (affiliation === "owner") ? html`<span class="badge badge-groupchat">${i18n_owner}</span>` : '' }
                         ${ (affiliation === "admin") ? html`<span class="badge badge-info">${i18n_admin}</span>` : '' }

+ 2 - 0
src/shared/chat/message.js

@@ -197,7 +197,9 @@ export default class Message extends CustomElement {
             'is_me_message': this.model.isMeCommand(),
             'is_retracted': this.isRetracted(),
             'username': this.model.getDisplayName(),
+            'color': this.model.occupant?.getColor(),
             'should_show_avatar': this.shouldShowAvatar(),
+            'colorize_username': api.settings.get('colorize_username'),
         }
     }
 

+ 4 - 3
src/shared/chat/templates/message.js

@@ -4,10 +4,11 @@ import { __ } from 'i18n';
 import { html } from "lit";
 import { shouldRenderMediaFromURL } from 'utils/url';
 
-
 export default (el, o) => {
     const i18n_new_messages = __('New messages');
     const is_followup = el.model.isFollowup();
+    const author_style = o.color ? 'color: ' + o.color + ' !important;' : '';
+
     return html`
         ${ o.is_first_unread ? html`<div class="message separator"><hr class="separator"><span class="separator-text">${ i18n_new_messages }</span></div>` : '' }
         <div class="message chat-msg ${ el.getExtraMessageClasses() }"
@@ -31,7 +32,7 @@ export default (el, o) => {
             <div class="chat-msg__content chat-msg__content--${o.sender} ${o.is_me_message ? 'chat-msg__content--action' : ''}">
                 ${ (!o.is_me_message && !is_followup) ? html`
                     <span class="chat-msg__heading">
-                        <span class="chat-msg__author"><a class="show-msg-author-modal" @click=${el.showUserModal}>${o.username}</a></span>
+                        <span class="chat-msg__author"><a class="show-msg-author-modal" @click=${el.showUserModal} style="${author_style}">${o.username}</a></span>
                         ${ o.hats.map(h => html`<span class="badge badge-secondary">${h.title}</span>`) }
                         <time timestamp="${el.model.get('edited') || el.model.get('time')}" class="chat-msg__time">${o.pretty_time}</time>
                         ${ o.is_encrypted ? html`<converse-icon class="fa fa-lock" size="1.1em"></converse-icon>` : '' }
@@ -41,7 +42,7 @@ export default (el, o) => {
                     <div class="chat-msg__message">
                         ${ (o.is_me_message) ? html`
                             <time timestamp="${o.edited || o.time}" class="chat-msg__time">${o.pretty_time}</time>&nbsp;
-                            <span class="chat-msg__author">${ o.is_me_message ? '**' : ''}${o.username}</span>&nbsp;` : '' }
+                            <span class="chat-msg__author" style="${author_style}">${ o.is_me_message ? '**' : ''}${o.username}</span>&nbsp;` : '' }
                         ${ o.is_retracted ? el.renderRetraction() : el.renderMessageText() }
                     </div>
                     <converse-message-actions

+ 2 - 0
src/types/shared/chat/message.d.ts

@@ -38,7 +38,9 @@ export default class Message extends CustomElement {
         is_me_message: any;
         is_retracted: any;
         username: any;
+        color: any;
         should_show_avatar: boolean;
+        colorize_username: any;
     };
     getRetractionText(): any;
     showUserModal(ev: any): void;