Browse Source

Allow opening the contact details modal from the roster

JC Brand 2 months ago
parent
commit
687d0be62f

+ 4 - 0
src/headless/shared/model-with-contact.js

@@ -61,6 +61,10 @@ export default function ModelWithContact(BaseModel) {
                     this.trigger('contact:change', changed);
                 });
 
+                this.listenTo(this.contact, 'destroy', () => {
+                    delete this.contact;
+                });
+
                 this.rosterContactAdded.resolve();
                 this.trigger('contactAdded', this.contact);
             }

+ 1 - 2
src/plugins/controlbox/styles/_controlbox.scss

@@ -137,8 +137,7 @@
             left: 0;
             overflow-x: hidden;
             overflow-y: auto;
-            padding: 0;
-            padding-top: 1em;
+            padding: 1em 0;
             text-align: start;
 
             .add-converse-contact {

+ 13 - 0
src/plugins/rosterview/contactview.js

@@ -70,6 +70,19 @@ export default class RosterContactView extends ObservableElement {
         await removeContact(this.model, true);
     }
 
+    /**
+     * @param {MouseEvent} ev
+     */
+    async showUserDetailsModal(ev) {
+        ev?.preventDefault?.();
+        ev.preventDefault();
+        if (this.model instanceof _converse.exports.Profile) {
+            api.modal.show('converse-profile-modal', { model: this.model }, ev);
+        } else {
+            api.modal.show('converse-user-details-modal', { model: this.model }, ev);
+        }
+    }
+
     /**
      * @param {MouseEvent} ev
      */

+ 5 - 2
src/plugins/rosterview/styles/roster.scss

@@ -52,7 +52,6 @@
                 width: 100%;
                 margin: 0;
                 padding: 0;
-                overflow: hidden;
                 white-space: nowrap;
                 text-overflow: ellipsis;
                 display: flex;
@@ -63,6 +62,11 @@
                 }
 
                 .contact-actions {
+                    .dropdown-item {
+                        converse-icon {
+                            margin-inline-end: 0.5em;
+                        }
+                    }
                     .list-item-action {
                         line-height: 2em;
                     }
@@ -72,7 +76,6 @@
                         }
                     }
                     converse-icon {
-                        padding-top: 0.5em;
                         svg {
                             fill: var(--chat-color) !important;
                         }

+ 82 - 50
src/plugins/rosterview/templates/roster_item.js

@@ -1,71 +1,103 @@
 import { __ } from 'i18n';
-import { _converse, api } from "@converse/headless";
-import { html } from "lit";
+import { _converse, api } from '@converse/headless';
+import { html } from 'lit';
 import { getUnreadMsgsDisplay } from 'shared/chat/utils.js';
 import { STATUSES } from '../constants.js';
 
 /**
  * @param {import('../contactview').default} el
  */
-export const tplRemoveLink = (el) => {
-   const display_name = el.model.getDisplayName();
-   const i18n_remove = __('Click to remove %1$s as a contact', display_name);
-   return html`
-      <a class="list-item-action remove-xmpp-contact" @click="${el.removeContact}" title="${i18n_remove}" href="#">
-         <converse-icon class="fa fa-trash-alt" size="1.5em"></converse-icon>
-      </a>
-   `;
-}
+export function tplRemoveButton(el) {
+    const display_name = el.model.getDisplayName();
+    const i18n_remove = __('Click to remove %1$s as a contact', display_name);
+    return html`<a
+        class="dropdown-item remove-xmpp-contact"
+        role="button"
+        @click="${el.removeContact}"
+        title="${i18n_remove}"
+        data-toggle="modal"
+    >
+        <converse-icon class="fa fa-trash-alt" size="1em"></converse-icon>
+        ${__('Remove')}
+    </a>`;
+};
 
 /**
  * @param {import('../contactview').default} el
  */
-export default  (el) => {
-   const bare_jid = _converse.session.get('bare_jid');
-   const show = el.model.getStatus() || 'offline';
+export function tplDetailsButton(el) {
+    const display_name = el.model.getDisplayName();
+    const i18n_remove = __('Click to show more details about %1$s', display_name);
+    return html`<a
+        class="dropdown-item"
+        role="button"
+        @click="${(ev) => el.showUserDetailsModal(ev)}"
+        title="${i18n_remove}"
+        data-toggle="modal"
+    >
+        <converse-icon class="fa fa-id-card" size="1em"></converse-icon>
+        ${__('Details')}
+    </a>`;
+};
+
+/**
+ * @param {import('../contactview').default} el
+ */
+export default (el) => {
+    const bare_jid = _converse.session.get('bare_jid');
+    const show = el.model.getStatus() || 'offline';
     let classes, color;
     if (show === 'online') {
         [classes, color] = ['fa fa-circle', 'chat-status-online'];
     } else if (show === 'dnd') {
-        [classes, color] =  ['fa fa-minus-circle', 'chat-status-busy'];
+        [classes, color] = ['fa fa-minus-circle', 'chat-status-busy'];
     } else if (show === 'away') {
-        [classes, color] =  ['fa fa-circle', 'chat-status-away'];
+        [classes, color] = ['fa fa-circle', 'chat-status-away'];
     } else {
         [classes, color] = ['fa fa-circle', 'chat-status-offline'];
     }
 
-   const is_self = bare_jid === el.model.get('jid');
-   const desc_status = STATUSES[show];
-   const num_unread = getUnreadMsgsDisplay(el.model);
-   const display_name = el.model.getDisplayName({ context: 'roster' });
-   const jid = el.model.get('jid');
-   const i18n_chat = is_self ?
-      __('Click to chat with yourself') :
-      __('Click to chat with %1$s (XMPP address: %2$s)', display_name, jid);
+    const is_self = bare_jid === el.model.get('jid');
+    const desc_status = STATUSES[show];
+    const num_unread = getUnreadMsgsDisplay(el.model);
+    const display_name = el.model.getDisplayName({ context: 'roster' });
+    const jid = el.model.get('jid');
+    const i18n_chat = is_self
+        ? __('Click to chat with yourself')
+        : __('Click to chat with %1$s (XMPP address: %2$s)', display_name, jid);
+
+    const btns = [
+       tplDetailsButton(el),
+       ...(api.settings.get('allow_contact_removal') && !is_self ? [tplRemoveButton(el)] : []),
+    ];
 
-   return html`
-      <a class="list-item-link cbox-list-item open-chat ${ num_unread ? 'unread-msgs' : '' }"
-         title="${i18n_chat}"
-         href="#"
-         data-jid=${jid}
-         @click=${el.openChat}>
-      <span>
-         <converse-avatar
-            .model=${el.model}
-            class="avatar"
-            name="${el.model.getDisplayName()}"
-            nonce=${el.model.vcard?.get('vcard_updated')}
-            height="30" width="30"></converse-avatar>
-         <converse-icon
-            title="${desc_status}"
-            color="var(--${color})"
-            size="1em"
-            class="${classes} chat-status chat-status--avatar"></converse-icon>
-      </span>
-      ${ num_unread ? html`<span class="msgs-indicator badge">${ num_unread }</span>` : '' }
-      <span class="contact-name contact-name--${show} ${ num_unread ? 'unread-msgs' : ''}">${display_name}</span>
-   </a>
-   <span class="contact-actions">
-      ${ api.settings.get('allow_contact_removal') && !is_self ? tplRemoveLink(el) : '' }
-   </span>`;
-}
+    return html` <a
+            class="list-item-link cbox-list-item open-chat ${num_unread ? 'unread-msgs' : ''}"
+            title="${i18n_chat}"
+            href="#"
+            data-jid=${jid}
+            @click=${el.openChat}
+        >
+            <span>
+                <converse-avatar
+                    .model=${el.model}
+                    class="avatar"
+                    name="${el.model.getDisplayName()}"
+                    nonce=${el.model.vcard?.get('vcard_updated')}
+                    height="30"
+                    width="30"
+                ></converse-avatar>
+                <converse-icon
+                    title="${desc_status}"
+                    color="var(--${color})"
+                    size="1em"
+                    class="${classes} chat-status chat-status--avatar"
+                ></converse-icon>
+            </span>
+            ${num_unread ? html`<span class="msgs-indicator badge">${num_unread}</span>` : ''}
+            <span class="contact-name contact-name--${show} ${num_unread ? 'unread-msgs' : ''}">${display_name}</span>
+        </a>
+        <span class="contact-actions">
+            <converse-dropdown class="btn-group dropstart list-item-action" .items=${btns}></converse-dropdown>
+        </span>`;
+};

+ 29 - 12
src/plugins/rosterview/templates/unsaved_contact.js

@@ -2,7 +2,27 @@ import { __ } from 'i18n';
 import { api } from '@converse/headless';
 import { html } from 'lit';
 import { getUnreadMsgsDisplay } from 'shared/chat/utils.js';
-import { tplRemoveLink } from './roster_item';
+import { tplDetailsButton, tplRemoveButton } from './roster_item';
+
+
+/**
+ * @param {import('../contactview').default} el
+ */
+function tplAddContactButton(el) {
+    const display_name = el.model.getDisplayName();
+    const i18n_add_contact = __('Click to add %1$s as a contact', display_name);
+    return html`<a
+        class="dropdown-item add-contact"
+        role="button"
+        @click="${el.addContact}"
+        title="${i18n_add_contact}"
+        data-toggle="modal"
+    >
+        <converse-icon class="fa fa-user-plus" size="1.5em"></converse-icon>
+        ${__('Save as contact')}
+    </a>`;
+}
+
 
 /**
  * @param {import('../contactview').default} el
@@ -12,8 +32,14 @@ export default (el) => {
     const display_name = el.model.getDisplayName();
     const jid = el.model.get('jid');
 
-    const i18n_add_contact = __('Click to add %1$s as a contact', display_name);
     const i18n_chat = __('Click to chat with %1$s (XMPP address: %2$s)', display_name, jid);
+
+    const btns = [
+       ...(api.settings.get('allow_contact_removal') ? [tplRemoveButton(el)] : []),
+       tplDetailsButton(el),
+       tplAddContactButton(el)
+    ];
+
     return html`
         <a
             class="list-item-link cbox-list-item open-chat ${num_unread ? 'unread-msgs' : ''}"
@@ -36,16 +62,7 @@ export default (el) => {
             <span class="contact-name ${num_unread ? 'unread-msgs' : ''}">${display_name}</span>
         </a>
         <span class="contact-actions">
-            <a
-                class="add-contact list-item-action"
-                @click="${(ev) => el.addContact(ev)}"
-                aria-label="${i18n_add_contact}"
-                title="${i18n_add_contact}"
-                href="#"
-            >
-                <converse-icon class="fa fa-user-plus" size="1.5em"></converse-icon>
-            </a>
-            ${api.settings.get('allow_contact_removal') ? tplRemoveLink(el) : ''}
+            <converse-dropdown class="btn-group dropstart list-item-action" .items=${btns}></converse-dropdown>
         </span>
     `;
 };

+ 2 - 2
src/shared/modals/templates/user-details.js

@@ -53,7 +53,7 @@ export function tplUserDetailsModal(el) {
     const vcard_json = vcard ? vcard.toJSON() : {};
     const o = { ...el.model.toJSON(), ...vcard_json };
 
-    const is_roster_contact = el.model.contact !== undefined;
+    const is_roster_contact = el.getContact() !== undefined;
     const allow_contact_removal = api.settings.get("allow_contact_removal");
 
     const domain = _converse.session.get("domain");
@@ -133,7 +133,7 @@ export function tplUserDetailsModal(el) {
         );
     }
 
-    const { contact } = el.model;
+    const contact = el.getContact();
     if (!contact) return ''; // Happens during tests
 
     const name = contact.get("nickname") || contact.vcard?.get('fullname');

+ 29 - 18
src/shared/modals/user-details.js

@@ -1,4 +1,4 @@
-import { api } from '@converse/headless';
+import { api, _converse } from '@converse/headless';
 import { blockContact, removeContact, unblockContact } from 'plugins/rosterview/utils.js';
 import BaseModal from 'plugins/modal/modal.js';
 import { __ } from 'i18n';
@@ -27,10 +27,21 @@ export default class UserDetailsModal extends BaseModal {
     addListeners() {
         this.listenTo(this.model, 'change', () => this.requestUpdate());
 
-        this.model.rosterContactAdded.then(() => this.registerContactEventHandlers());
+        if (this.model instanceof _converse.exports.ChatBox) {
+            this.model.rosterContactAdded.then(() => this.registerContactEventHandlers(this.model.contact));
+            if (this.model.contact !== undefined) {
+                this.registerContactEventHandlers(this.model.contact);
+            }
+        } else {
+            this.registerContactEventHandlers(this.model);
+        }
+    }
 
-        if (this.model.contact !== undefined) {
-            this.registerContactEventHandlers();
+    getContact() {
+        if (this.model instanceof _converse.exports.ChatBox) {
+            return this.model.contact;
+        } else {
+            return this.model;
         }
     }
 
@@ -55,16 +66,16 @@ export default class UserDetailsModal extends BaseModal {
         return this.model.getDisplayName();
     }
 
-    registerContactEventHandlers() {
-        this.listenTo(this.model.contact, 'change', () => this.requestUpdate());
-        this.listenTo(this.model.contact.vcard, 'change', () => this.requestUpdate());
-        this.model.contact.on('destroy', () => {
-            delete this.model.contact;
-            this.close();
-        });
-
-        // Refresh the vcard
-        api.vcard.update(this.model.contact.vcard, true);
+    /**
+     * @param {import('@converse/headless/types/plugins/roster/contact').default} contact
+     */
+    registerContactEventHandlers(contact) {
+        this.listenTo(contact, 'change', () => this.requestUpdate());
+        this.listenTo(contact, 'destroy', () => this.close());
+        this.listenTo(contact.vcard, 'change', () => this.requestUpdate());
+        if (contact.vcard) { // Refresh the vcard
+            api.vcard.update(contact.vcard, true);
+        }
     }
 
     /**
@@ -76,7 +87,7 @@ export default class UserDetailsModal extends BaseModal {
         const data = new FormData(form);
         const name = /** @type {string} */ (data.get('name') || '').trim();
         const groups = /** @type {string} */ (data.get('groups'))?.split(',').map((g) => g.trim()) || [];
-        this.model.contact.update({
+        this.getContact().update({
             nickname: name,
             groups,
         });
@@ -88,7 +99,7 @@ export default class UserDetailsModal extends BaseModal {
      */
     async removeContact(ev) {
         ev?.preventDefault?.();
-        setTimeout(() => removeContact(this.model.contact), 1);
+        setTimeout(() => removeContact(this.getContact()), 1);
         this.modal.hide();
     }
 
@@ -97,7 +108,7 @@ export default class UserDetailsModal extends BaseModal {
      */
     async blockContact(ev) {
         ev?.preventDefault?.();
-        setTimeout(() => blockContact(this.model.contact), 1);
+        setTimeout(() => blockContact(this.getContact()), 1);
         this.modal.hide();
     }
 
@@ -106,7 +117,7 @@ export default class UserDetailsModal extends BaseModal {
      */
     async unblockContact(ev) {
         ev?.preventDefault?.();
-        setTimeout(() => unblockContact(this.model.contact), 1);
+        setTimeout(() => unblockContact(this.getContact()), 1);
         this.modal.hide();
     }
 }

+ 0 - 1
src/shared/styles/lists.scss

@@ -31,7 +31,6 @@
             border: none;
             clear: both;
             color: var(--text-color);
-            overflow: hidden;
             padding: 0.5em 0;
             word-wrap: break-word;
             height: 2.5em;

+ 4 - 0
src/types/plugins/rosterview/contactview.d.ts

@@ -24,6 +24,10 @@ export default class RosterContactView extends ObservableElement {
      * @param {MouseEvent} ev
      */
     removeContact(ev: MouseEvent): Promise<void>;
+    /**
+     * @param {MouseEvent} ev
+     */
+    showUserDetailsModal(ev: MouseEvent): Promise<void>;
     /**
      * @param {MouseEvent} ev
      */

+ 8 - 1
src/types/plugins/rosterview/templates/roster_item.d.ts

@@ -1,4 +1,11 @@
-export function tplRemoveLink(el: import("../contactview").default): import("lit-html").TemplateResult<1>;
+/**
+ * @param {import('../contactview').default} el
+ */
+export function tplRemoveButton(el: import("../contactview").default): import("lit-html").TemplateResult<1>;
+/**
+ * @param {import('../contactview').default} el
+ */
+export function tplDetailsButton(el: import("../contactview").default): import("lit-html").TemplateResult<1>;
 declare function _default(el: import("../contactview").default): import("lit-html").TemplateResult<1>;
 export default _default;
 //# sourceMappingURL=roster_item.d.ts.map

+ 6 - 2
src/types/shared/modals/user-details.d.ts

@@ -1,13 +1,17 @@
 export default class UserDetailsModal extends BaseModal {
     constructor(options: any);
     addListeners(): void;
+    getContact(): any;
     /**
      * @param {Map<string, any>} changed
      */
     shouldUpdate(changed: Map<string, any>): boolean;
     renderModal(): import("lit-html").TemplateResult<1> | "";
     getModalTitle(): any;
-    registerContactEventHandlers(): void;
+    /**
+     * @param {import('@converse/headless/types/plugins/roster/contact').default} contact
+     */
+    registerContactEventHandlers(contact: import("@converse/headless/types/plugins/roster/contact").default): void;
     /**
      * @param {MouseEvent} ev
      */
@@ -25,5 +29,5 @@ export default class UserDetailsModal extends BaseModal {
      */
     unblockContact(ev: MouseEvent): Promise<void>;
 }
-import BaseModal from "plugins/modal/modal.js";
+import BaseModal from 'plugins/modal/modal.js';
 //# sourceMappingURL=user-details.d.ts.map