Parcourir la source

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 il y a 1 an
Parent
commit
1e56addaf8

+ 1 - 1
.eslintrc.json

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

+ 1 - 0
dev.html

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

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

@@ -1,6 +1,9 @@
 import { Model } from '@converse/skeletor';
 import { Model } from '@converse/skeletor';
 import api from '../../shared/api/index.js';
 import api from '../../shared/api/index.js';
 import { AFFILIATIONS, ROLES } from './constants.js';
 import { AFFILIATIONS, ROLES } from './constants.js';
+import u from '../../utils/index.js';
+
+const { safeSave, colorize } = u;
 
 
 /**
 /**
  * Represents a participant in a MUC
  * Represents a participant in a MUC
@@ -15,6 +18,11 @@ class MUCOccupant extends Model {
         this.vcard = null;
         this.vcard = null;
     }
     }
 
 
+    initialize () {
+        this.on('change:nick', () => this.setColor());
+        this.on('change:jid', () => this.setColor());
+    }
+
     defaults () {
     defaults () {
         return {
         return {
             hats: [],
             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 () {
     isMember () {

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

@@ -1,5 +1,8 @@
 /**
 /**
  * @typedef {module:plugin-muc-parsers.MemberListItem} MemberListItem
  * @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 MUCOccupant from './occupant.js';
 import _converse from '../../shared/_converse.js';
 import _converse from '../../shared/_converse.js';
@@ -11,7 +14,6 @@ import { Strophe } from 'strophe.js';
 import { getAffiliationList } from './affiliations/utils.js';
 import { getAffiliationList } from './affiliations/utils.js';
 import { occupantsComparator } from './utils.js';
 import { occupantsComparator } from './utils.js';
 import { getUniqueId } from '../../utils/index.js';
 import { getUniqueId } from '../../utils/index.js';
-import { colorize } from '../../utils/color';
 
 
 const { u } = converse.env;
 const { u } = converse.env;
 
 
@@ -23,6 +25,10 @@ const { u } = converse.env;
  */
  */
 class MUCOccupants extends Collection {
 class MUCOccupants extends Collection {
 
 
+    /**
+     * @param {MUCOccupant[]} attrs
+     * @param {CollectionOptions} options
+     */
     constructor (attrs, options) {
     constructor (attrs, options) {
         super(
         super(
             attrs,
             attrs,
@@ -37,24 +43,18 @@ class MUCOccupants extends Collection {
 
 
     initialize() {
     initialize() {
         this.on('change:nick', () => this.sort());
         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());
         this.on('change:role', () => this.sort());
     }
     }
 
 
-
     static getAutoFetchedAffiliationLists () {
     static getAutoFetchedAffiliationLists () {
         const affs = api.settings.get('muc_fetch_members');
         const affs = api.settings.get('muc_fetch_members');
         return Array.isArray(affs) ? affs : affs ? ['member', 'admin', 'owner'] : [];
         return Array.isArray(affs) ? affs : affs ? ['member', 'admin', 'owner'] : [];
     }
     }
 
 
+    /**
+     * @param {Model|Attributes} attrs
+     * @param {Options} [options]
+     */
     create (attrs, options) {
     create (attrs, options) {
         if (attrs.id || attrs instanceof Model) {
         if (attrs.id || attrs instanceof Model) {
             return super.create(attrs, options);
             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 default MUCOccupants;
 export type MemberListItem = any;
 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.
  * A list of {@link MUCOccupant} instances, representing participants in a MUC.
  * @class
  * @class
@@ -7,16 +10,13 @@ export type MemberListItem = any;
  */
  */
 declare class MUCOccupants extends Collection {
 declare class MUCOccupants extends Collection {
     static getAutoFetchedAffiliationLists(): any[];
     static getAutoFetchedAffiliationLists(): any[];
-    constructor(attrs: any, options: any);
+    /**
+     * @param {MUCOccupant[]} attrs
+     * @param {CollectionOptions} options
+     */
+    constructor(attrs: MUCOccupant[], options: CollectionOptions);
     chatroom: any;
     chatroom: any;
     get model(): typeof MUCOccupant;
     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>;
     fetchMembers(): Promise<void>;
     /**
     /**
      * @typedef { Object} OccupantData
      * @typedef { Object} OccupantData
@@ -56,5 +56,4 @@ declare class MUCOccupants extends Collection {
 }
 }
 import { Collection } from "@converse/skeletor";
 import { Collection } from "@converse/skeletor";
 import MUCOccupant from "./occupant.js";
 import MUCOccupant from "./occupant.js";
-import { Model } from "@converse/skeletor";
 //# sourceMappingURL=occupants.d.ts.map
 //# 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;
     isMentionBoundary(s: string): boolean;
     replaceCurrentWord(input: HTMLInputElement, new_value: string): void;
     replaceCurrentWord(input: HTMLInputElement, new_value: string): void;
     placeCaretAtEnd(textarea: HTMLTextAreaElement): void;
     placeCaretAtEnd(textarea: HTMLTextAreaElement): void;
+    colorize(s: string): Promise<string>;
     /**
     /**
      * @copyright The Converse.js contributors
      * @copyright The Converse.js contributors
      * @license Mozilla Public License (MPLv2)
      * @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 { Model } from '@converse/skeletor';
 import log, { LEVELS } from '../log.js';
 import log, { LEVELS } from '../log.js';
 import { waitUntil } from './promise.js';
 import { waitUntil } from './promise.js';
+import * as color from './color.js';
 import * as stanza from './stanza.js';
 import * as stanza from './stanza.js';
 import * as session from './session.js';
 import * as session from './session.js';
 import * as object from './object.js';
 import * as object from './object.js';
@@ -165,6 +166,7 @@ export function getUniqueId (suffix) {
 
 
 export default Object.assign({
 export default Object.assign({
     ...arraybuffer,
     ...arraybuffer,
+    ...color,
     ...form,
     ...form,
     ...html,
     ...html,
     ...jid,
     ...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 { __ } from 'i18n';
 import { html } from "lit";
 import { html } from "lit";
 import { showOccupantModal } from '../utils.js';
 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'))
 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'];
         [classes, color] = ['fa fa-circle', 'subdued-color'];
     }
     }
 
 
-    const occupant_color = o.getColor();
-    const occupant_style = color ? 'color: ' + occupant_color + ' !important;' : '';
-
     return html`
     return html`
         <li class="occupant" id="${o.id}" title="${occupant_title(o)}">
         <li class="occupant" id="${o.id}" title="${occupant_title(o)}">
             <div class="row no-gutters">
             <div class="row no-gutters">
@@ -68,7 +66,7 @@ export default (o, chat) => {
                     </a>
                     </a>
                 </div>
                 </div>
                 <div class="col occupant-nick-badge">
                 <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">
                     <span class="occupant-badges">
                         ${ (affiliation === "owner") ? html`<span class="badge badge-groupchat">${i18n_owner}</span>` : '' }
                         ${ (affiliation === "owner") ? html`<span class="badge badge-groupchat">${i18n_owner}</span>` : '' }
                         ${ (affiliation === "admin") ? html`<span class="badge badge-info">${i18n_admin}</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_me_message': this.model.isMeCommand(),
             'is_retracted': this.isRetracted(),
             'is_retracted': this.isRetracted(),
             'username': this.model.getDisplayName(),
             'username': this.model.getDisplayName(),
-            'color': this.model.occupant?.getColor(),
             'should_show_avatar': this.shouldShowAvatar(),
             'should_show_avatar': this.shouldShowAvatar(),
             'colorize_username': api.settings.get('colorize_username'),
             '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/avatar/avatar.js';
 import 'shared/chat/unfurl.js';
 import 'shared/chat/unfurl.js';
 import { __ } from 'i18n';
 import { __ } from 'i18n';
 import { html } from "lit";
 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) => {
 export default (el, o) => {
     const i18n_new_messages = __('New messages');
     const i18n_new_messages = __('New messages');
     const is_followup = el.model.isFollowup();
     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`
     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() }"
         <div class="message chat-msg ${ el.getExtraMessageClasses() }"
                 data-isodate="${o.time}"
                 data-isodate="${o.time}"
                 data-msgid="${o.msgid}"
                 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' : ''}">
             <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`
                 ${ (!o.is_me_message && !is_followup) ? html`
                     <span class="chat-msg__heading">
                     <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>`) }
                         ${ 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>
                         <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>` : '' }
                         ${ 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_me_message: any;
         is_retracted: any;
         is_retracted: any;
         username: any;
         username: any;
-        color: any;
         should_show_avatar: boolean;
         should_show_avatar: boolean;
         colorize_username: any;
         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 default _default;
+export type Message = import('shared/chat/message').default;
 //# sourceMappingURL=message.d.ts.map
 //# 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), '')}` : '';
+        }
+    }
+}