瀏覽代碼

Lazily compute the consistent colors for nicknames

When calling `occupant.getColor()` I think a color should always be
returned, but this means making the function async, and thereby using
`until` in the templates.

This change also only generates the color on demand, i.e. lazily,
instead of eagerly.

The benefit is that we avoid generating colors that aren't being used
because the occupant isn't being shown anywhere in the UI.
JC Brand 1 年之前
父節點
當前提交
1e56addaf8

+ 1 - 1
.eslintrc.json

@@ -167,7 +167,7 @@
         "no-restricted-properties": "error",
         "no-restricted-syntax": "error",
         "no-return-assign": "error",
-        "no-return-await": "error",
+        "no-return-await": "off",
         "no-script-url": "error",
         "no-self-compare": "error",
         "no-sequences": "error",

+ 1 - 0
dev.html

@@ -28,6 +28,7 @@
     });
 
     converse.initialize({
+        colorize_username: true,
         i18n: 'af',
         theme: 'dracula',
         auto_away: 300,

+ 18 - 2
src/headless/plugins/muc/occupant.js

@@ -1,6 +1,9 @@
 import { Model } from '@converse/skeletor';
 import api from '../../shared/api/index.js';
 import { AFFILIATIONS, ROLES } from './constants.js';
+import u from '../../utils/index.js';
+
+const { safeSave, colorize } = u;
 
 /**
  * Represents a participant in a MUC
@@ -15,6 +18,11 @@ class MUCOccupant extends Model {
         this.vcard = null;
     }
 
+    initialize () {
+        this.on('change:nick', () => this.setColor());
+        this.on('change:jid', () => this.setColor());
+    }
+
     defaults () {
         return {
             hats: [],
@@ -78,8 +86,16 @@ class MUCOccupant extends Model {
         }
     }
 
-    getColor () {
-        return this.get('color') || '';
+    async setColor () {
+        const color = await colorize(this.getDisplayName());
+        safeSave(this, { color });
+    }
+
+    async getColor () {
+        if (!this.get('color')) {
+            await this.setColor();
+        }
+        return this.get('color');
     }
 
     isMember () {

+ 11 - 11
src/headless/plugins/muc/occupants.js

@@ -1,5 +1,8 @@
 /**
  * @typedef {module:plugin-muc-parsers.MemberListItem} MemberListItem
+ * @typedef {import('@converse/skeletor/src/types/collection').Attributes} Attributes
+ * @typedef {import('@converse/skeletor/src/types/collection').CollectionOptions} CollectionOptions
+ * @typedef {import('@converse/skeletor/src/types/collection').Options} Options
  */
 import MUCOccupant from './occupant.js';
 import _converse from '../../shared/_converse.js';
@@ -11,7 +14,6 @@ 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;
 
@@ -23,6 +25,10 @@ const { u } = converse.env;
  */
 class MUCOccupants extends Collection {
 
+    /**
+     * @param {MUCOccupant[]} attrs
+     * @param {CollectionOptions} options
+     */
     constructor (attrs, options) {
         super(
             attrs,
@@ -37,24 +43,18 @@ 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());
     }
 
-
     static getAutoFetchedAffiliationLists () {
         const affs = api.settings.get('muc_fetch_members');
         return Array.isArray(affs) ? affs : affs ? ['member', 'admin', 'owner'] : [];
     }
 
+    /**
+     * @param {Model|Attributes} attrs
+     * @param {Options} [options]
+     */
     create (attrs, options) {
         if (attrs.id || attrs instanceof Model) {
             return super.create(attrs, options);

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

@@ -0,0 +1,37 @@
+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;
+    setColor(): Promise<void>;
+    getColor(): Promise<any>;
+    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

+ 8 - 9
src/headless/types/plugins/muc/occupants.d.ts

@@ -1,5 +1,8 @@
 export default MUCOccupants;
 export type MemberListItem = any;
+export type Attributes = import('@converse/skeletor/src/types/collection').Attributes;
+export type CollectionOptions = import('@converse/skeletor/src/types/collection').CollectionOptions;
+export type Options = import('@converse/skeletor/src/types/collection').Options;
 /**
  * A list of {@link MUCOccupant} instances, representing participants in a MUC.
  * @class
@@ -7,16 +10,13 @@ export type MemberListItem = any;
  */
 declare class MUCOccupants extends Collection {
     static getAutoFetchedAffiliationLists(): any[];
-    constructor(attrs: any, options: any);
+    /**
+     * @param {MUCOccupant[]} attrs
+     * @param {CollectionOptions} options
+     */
+    constructor(attrs: MUCOccupant[], options: CollectionOptions);
     chatroom: any;
     get model(): typeof MUCOccupant;
-    create(attrs: any, options: any): false | Model | import("@converse/skeletor/src/types/collection.js").Attributes | (Promise<any> & {
-        isResolved: boolean;
-        isPending: boolean;
-        isRejected: boolean;
-        resolve: Function;
-        reject: Function;
-    });
     fetchMembers(): Promise<void>;
     /**
      * @typedef { Object} OccupantData
@@ -56,5 +56,4 @@ declare class MUCOccupants extends Collection {
 }
 import { Collection } from "@converse/skeletor";
 import MUCOccupant from "./occupant.js";
-import { Model } from "@converse/skeletor";
 //# sourceMappingURL=occupants.d.ts.map

+ 1 - 0
src/headless/types/utils/index.d.ts

@@ -83,6 +83,7 @@ declare const _default: {
     isMentionBoundary(s: string): boolean;
     replaceCurrentWord(input: HTMLInputElement, new_value: string): void;
     placeCaretAtEnd(textarea: HTMLTextAreaElement): void;
+    colorize(s: string): Promise<string>;
     /**
      * @copyright The Converse.js contributors
      * @license Mozilla Public License (MPLv2)

+ 2 - 0
src/headless/utils/index.js

@@ -8,6 +8,7 @@ import { getOpenPromise } from '@converse/openpromise';
 import { Model } from '@converse/skeletor';
 import log, { LEVELS } from '../log.js';
 import { waitUntil } from './promise.js';
+import * as color from './color.js';
 import * as stanza from './stanza.js';
 import * as session from './session.js';
 import * as object from './object.js';
@@ -165,6 +166,7 @@ export function getUniqueId (suffix) {
 
 export default Object.assign({
     ...arraybuffer,
+    ...color,
     ...form,
     ...html,
     ...jid,

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

@@ -2,6 +2,7 @@ import { PRETTY_CHAT_STATUS } from '../constants.js';
 import { __ } from 'i18n';
 import { html } from "lit";
 import { showOccupantModal } from '../utils.js';
+import { getAuthorStyle } from 'utils/color.js';
 
 const i18n_occupant_hint = (o) => __('Click to mention %1$s in your message.', o.get('nick'))
 
@@ -46,9 +47,6 @@ 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">
@@ -68,7 +66,7 @@ export default (o, chat) => {
                     </a>
                 </div>
                 <div class="col occupant-nick-badge">
-                    <span class="occupant-nick" @click=${chat.onOccupantClicked} style="${occupant_style}">${o.getDisplayName()}</span>
+                    <span class="occupant-nick" @click=${chat.onOccupantClicked} style="${getAuthorStyle(o)}">${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>` : '' }

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

@@ -197,7 +197,6 @@ 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'),
         }

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

@@ -1,16 +1,29 @@
+/**
+ * @typedef {import('shared/chat/message').default} Message
+ */
 import 'shared/avatar/avatar.js';
 import 'shared/chat/unfurl.js';
 import { __ } from 'i18n';
 import { html } from "lit";
-import { shouldRenderMediaFromURL } from 'utils/url';
+import { shouldRenderMediaFromURL } from 'utils/url.js';
+import { getAuthorStyle } from 'utils/color.js';
 
+
+/**
+ * @param {Message} el
+ * @param {Object} o
+ */
 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;' : '';
+    const occupant = el.model.occupant;
+    const author_style = getAuthorStyle(occupant);
 
     return html`
-        ${ o.is_first_unread ? html`<div class="message separator"><hr class="separator"><span class="separator-text">${ i18n_new_messages }</span></div>` : '' }
+        ${ 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() }"
                 data-isodate="${o.time}"
                 data-msgid="${o.msgid}"
@@ -32,7 +45,11 @@ 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} style="${author_style}">${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>` : '' }

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

@@ -38,7 +38,6 @@ export default class Message extends CustomElement {
         is_me_message: any;
         is_retracted: any;
         username: any;
-        color: any;
         should_show_avatar: boolean;
         colorize_username: any;
     };

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

@@ -1,3 +1,4 @@
-declare function _default(el: any, o: any): import("lit-html").TemplateResult<1>;
+declare function _default(el: Message, o: any): import("lit-html").TemplateResult<1>;
 export default _default;
+export type Message = import('shared/chat/message').default;
 //# sourceMappingURL=message.d.ts.map

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

@@ -0,0 +1,9 @@
+/**
+ * @param {MUCOccupant} occupant
+ * @returns {string|TemplateResult}
+ */
+export function getAuthorStyle(occupant: MUCOccupant): string | TemplateResult;
+export type TemplateResult = import('lit').TemplateResult;
+export type Message = import('shared/chat/message').default;
+export type MUCOccupant = import('@converse/headless/types/plugins/muc/occupant').default;
+//# sourceMappingURL=color.d.ts.map

+ 28 - 0
src/utils/color.js

@@ -0,0 +1,28 @@
+/**
+ * @typedef {import('lit').TemplateResult} TemplateResult
+ * @typedef {import('shared/chat/message').default} Message
+ * @typedef {import('@converse/headless/types/plugins/muc/occupant').default} MUCOccupant
+ */
+import { html } from "lit";
+import { until } from 'lit/directives/until.js';
+import { api } from '@converse/headless';
+
+/** @param {string} color */
+function getStyle (color) {
+    return `color: ${color}!important;`;
+}
+
+/**
+ * @param {MUCOccupant} occupant
+ * @returns {string|TemplateResult}
+ */
+export function getAuthorStyle (occupant) {
+    if (api.settings.get('colorize_username')) {
+        const color = occupant?.get('color');
+        if (color) {
+            return getStyle(color);
+        } else {
+            return occupant ? html`${until(occupant?.getColor().then(getStyle), '')}` : '';
+        }
+    }
+}