فهرست منبع

Allow user modals to be opened from message headings

JC Brand 4 سال پیش
والد
کامیت
34cba68432

+ 5 - 0
sass/_chatbox.scss

@@ -1,4 +1,5 @@
 #conversejs {
+
     .chatbox-navback {
         display: none;
     }
@@ -52,6 +53,10 @@
             margin-right: 0.5em;
         }
 
+        .show-msg-author-modal {
+            color: #ffffff !important;
+        }
+
         .chat-head__desc {
             color: var(--chat-head-color-lighten-50-percent);
             font-size: var(--font-size-small);

+ 4 - 0
sass/_messages.scss

@@ -9,6 +9,10 @@
         }
     }
     .message {
+        .show-msg-author-modal {
+            color: var(--text-color) !important;
+        }
+
         blockquote {
             margin-left: 0.5em;
             margin-bottom: 0.25em;

+ 10 - 0
src/components/message.js

@@ -268,6 +268,16 @@ export default class Message extends CustomElement {
         `;
     }
 
+    showUserModal (ev) {
+        if (this.model.get('sender') === 'me') {
+            _converse.xmppstatusview.showProfileModal(ev);
+        } else if (this.message_type === 'groupchat') {
+            this.chatview.showOccupantDetailsModal(ev, this.model);
+        } else {
+            this.chatview.showUserDetailsModal(ev, this.model);
+        }
+    }
+
     showMessageVersionsModal (ev) {
         ev.preventDefault();
         if (this.message_versions_modal === undefined) {

+ 26 - 110
src/converse-chatview.js

@@ -3,24 +3,23 @@
  * @copyright 2020, the Converse.js contributors
  * @license Mozilla Public License (MPLv2)
  */
-import "./components/chat_content.js";
-import "./components/help_messages.js";
-import "./components/toolbar.js";
-import "converse-chatboxviews";
-import "converse-modal";
-import log from "@converse/headless/log";
-import tpl_chatbox from "templates/chatbox.js";
-import tpl_chatbox_head from "templates/chatbox_head.js";
-import tpl_chatbox_message_form from "templates/chatbox_message_form.js";
-import tpl_spinner from "templates/spinner.js";
-import tpl_toolbar from "templates/toolbar.js";
-import tpl_user_details_modal from "templates/user_details_modal.js";
-import { BootstrapModal } from "./converse-modal.js";
+import './components/chat_content.js';
+import './components/help_messages.js';
+import './components/toolbar.js';
+import 'converse-chatboxviews';
+import 'converse-modal';
+import log from '@converse/headless/log';
+import tpl_chatbox from 'templates/chatbox.js';
+import tpl_chatbox_head from 'templates/chatbox_head.js';
+import tpl_chatbox_message_form from 'templates/chatbox_message_form.js';
+import tpl_spinner from 'templates/spinner.js';
+import tpl_toolbar from 'templates/toolbar.js';
+import UserDetailsModal from 'modals/user-details.js';
 import { View } from '@converse/skeletor/src/view.js';
 import { __ } from './i18n';
-import { _converse, api, converse } from "@converse/headless/converse-core";
-import { debounce } from "lodash-es";
-import { html, render } from "lit-html";
+import { _converse, api, converse } from '@converse/headless/converse-core';
+import { debounce } from 'lodash-es';
+import { html, render } from 'lit-html';
 
 
 const { Strophe, dayjs } = converse.env;
@@ -240,7 +239,7 @@ export const ChatBoxView = View.extend({
     showUserDetailsModal (ev) {
         ev.preventDefault();
         if (this.user_details_modal === undefined) {
-            this.user_details_modal = new _converse.UserDetailsModal({model: this.model});
+            this.user_details_modal = new UserDetailsModal({model: this.model});
         }
         this.user_details_modal.show(ev);
     },
@@ -283,17 +282,24 @@ export const ChatBoxView = View.extend({
     async generateHeadingTemplate () {
         const vcard = this.model?.vcard;
         const vcard_json = vcard ? vcard.toJSON() : {};
+        const i18n_profile = __('The User\'s Profile Image');
+        const avatar_data = Object.assign({
+            'alt_text': i18n_profile,
+            'extra_classes': '',
+            'height': 40,
+            'width': 40,
+        }, vcard_json);
         const heading_btns = await this.getHeadingButtons();
         const standalone_btns = heading_btns.filter(b => b.standalone);
         const dropdown_btns = heading_btns.filter(b => !b.standalone);
         return tpl_chatbox_head(
             Object.assign(
-                vcard_json,
                 this.model.toJSON(), {
-                    '_converse': _converse,
+                    avatar_data,
+                    'display_name': this.model.getDisplayName(),
                     'dropdown_btns': dropdown_btns.map(b => this.getHeadingDropdownItem(b)),
+                    'showUserDetailsModal': ev => this.showUserDetailsModal(ev),
                     'standalone_btns': standalone_btns.map(b => this.getHeadingStandaloneButton(b)),
-                    'display_name': this.model.getDisplayName()
                 }
             )
         );
@@ -1053,96 +1059,6 @@ converse.plugins.add('converse-chatview', {
 
         _converse.ChatBoxView = ChatBoxView;
 
-
-        _converse.UserDetailsModal = BootstrapModal.extend({
-            id: "user-details-modal",
-
-            events: {
-                'click button.refresh-contact': 'refreshContact',
-                'click .fingerprint-trust .btn input': 'toggleDeviceTrust'
-            },
-
-            initialize () {
-                BootstrapModal.prototype.initialize.apply(this, arguments);
-                this.model.rosterContactAdded.then(() => this.registerContactEventHandlers());
-                this.listenTo(this.model, 'change', this.render);
-                this.registerContactEventHandlers();
-                /**
-                 * Triggered once the _converse.UserDetailsModal has been initialized
-                 * @event _converse#userDetailsModalInitialized
-                 * @type { _converse.ChatBox }
-                 * @example _converse.api.listen.on('userDetailsModalInitialized', chatbox => { ... });
-                 */
-                api.trigger('userDetailsModalInitialized', this.model);
-            },
-
-            toHTML () {
-                const vcard = this.model?.vcard;
-                const vcard_json = vcard ? vcard.toJSON() : {};
-                return tpl_user_details_modal(Object.assign(
-                    this.model.toJSON(),
-                    vcard_json, {
-                    '_converse': _converse,
-                    'allow_contact_removal': api.settings.get('allow_contact_removal'),
-                    'display_name': this.model.getDisplayName(),
-                    'is_roster_contact': this.model.contact !== undefined,
-                    'removeContact': ev => this.removeContact(ev),
-                    'view': this,
-                    'utils': u
-                }));
-            },
-
-            registerContactEventHandlers () {
-                if (this.model.contact !== undefined) {
-                    this.listenTo(this.model.contact, 'change', this.render);
-                    this.listenTo(this.model.contact.vcard, 'change', this.render);
-                    this.model.contact.on('destroy', () => {
-                        delete this.model.contact;
-                        this.render();
-                    });
-                }
-            },
-
-            async refreshContact (ev) {
-                if (ev && ev.preventDefault) { ev.preventDefault(); }
-                const refresh_icon = this.el.querySelector('.fa-refresh');
-                u.addClass('fa-spin', refresh_icon);
-                try {
-                    await api.vcard.update(this.model.contact.vcard, true);
-                } catch (e) {
-                    log.fatal(e);
-                    this.alert(__('Sorry, something went wrong while trying to refresh'), 'danger');
-                }
-                u.removeClass('fa-spin', refresh_icon);
-            },
-
-            removeContact (ev) {
-                if (ev && ev.preventDefault) { ev.preventDefault(); }
-                if (!api.settings.get('allow_contact_removal')) { return; }
-                const result = confirm(__("Are you sure you want to remove this contact?"));
-                if (result === true) {
-                    this.modal.hide();
-                    // XXX: This is annoying but necessary to get tests to pass.
-                    // The `dismissHandler` in bootstrap.native tries to
-                    // reference the remove button after it's been cleared from
-                    // the DOM, so we delay removing the contact to give it time.
-                    setTimeout(() => {
-                        this.model.contact.removeFromRoster(
-                            () => this.model.contact.destroy(),
-                            (err) => {
-                                log.error(err);
-                                api.alert('error', __('Error'), [
-                                    __('Sorry, there was an error while trying to remove %1$s as a contact.',
-                                    this.model.contact.getDisplayName())
-                                ]);
-                            }
-                        );
-                    }, 1);
-                }
-            },
-        });
-
-
         api.listen.on('chatBoxViewsInitialized', () => {
             const views = _converse.chatboxviews;
             _converse.chatboxes.on('add', async item => {

+ 9 - 0
src/converse-muc-views.js

@@ -11,6 +11,7 @@ import AddMUCModal from 'modals/add-muc.js';
 import MUCInviteModal from 'modals/muc-invite.js';
 import MUCListModal from 'modals/muc-list.js';
 import ModeratorToolsModal from "./modals/moderator-tools.js";
+import OccupantModal from 'modals/occupant.js';
 import RoomDetailsModal from 'modals/muc-details.js';
 import log from "@converse/headless/log";
 import tpl_chatroom from "templates/chatroom.js";
@@ -517,6 +518,14 @@ export const ChatRoomView = ChatBoxView.extend({
         this.model.room_details_modal.show(ev);
     },
 
+    showOccupantDetailsModal (ev, message) {
+        ev.preventDefault();
+        if (this.model.occupant_modal === undefined) {
+            this.model.occupant_modal = new OccupantModal({'model': message.occupant});
+        }
+        this.model.occupant_modal.show(ev);
+    },
+
     showChatStateNotification (message) {
         if (message.get('sender') === 'me') {
             return;

+ 1 - 0
src/converse-omemo.js

@@ -6,6 +6,7 @@
 /* global libsignal */
 
 import "converse-profile";
+import 'modals/user-details.js';
 import log from "@converse/headless/log";
 import { Collection } from "@converse/skeletor/src/collection";
 import { Model } from '@converse/skeletor/src/model.js';

+ 47 - 0
src/modals/occupant.js

@@ -0,0 +1,47 @@
+import tpl_occupant_modal from "./templates/occupant.js";
+import { BootstrapModal } from "../converse-modal.js";
+import { _converse, api } from "@converse/headless/converse-core";
+
+
+const OccupantModal = BootstrapModal.extend({
+    id: "muc-occupant-modal",
+
+    initialize () {
+        BootstrapModal.prototype.initialize.apply(this, arguments);
+        this.listenTo(this.model, 'change', this.render);
+        /**
+            * Triggered once the OccupantModal has been initialized
+            * @event _converse#userDetailsModalInitialized
+            * @type { _converse.ChatBox }
+            * @example _converse.api.listen.on('userDetailsModalInitialized', chatbox => { ... });
+            */
+        api.trigger('occupantModalInitialized', this.model);
+    },
+
+    toHTML () {
+        return tpl_occupant_modal(Object.assign(
+            this.model.toJSON(),
+            {
+                'avatar_data': this.getAvatarData(),
+                'display_name': this.model.getDisplayName()
+            }
+        ));
+    },
+
+    getAvatarData () {
+        const vcard = _converse.vcards.findWhere({'jid': this.model.get('jid')});
+        const image_type = vcard?.get('image_type') || _converse.DEFAULT_IMAGE_TYPE;
+        const image_data = vcard?.get('image') || _converse.DEFAULT_IMAGE;
+        const image = "data:" + image_type + ";base64," + image_data;
+        return {
+            'classes': 'chat-msg__avatar',
+            'height': 120,
+            'width': 120,
+            image,
+        };
+    }
+});
+
+_converse.OccupantModal = OccupantModal;
+
+export default OccupantModal;

+ 23 - 0
src/modals/templates/occupant.js

@@ -0,0 +1,23 @@
+import { html } from "lit-html";
+import { modal_close_button, modal_header_close_button } from "../../templates/buttons"
+import { renderAvatar } from '../../templates/directives/avatar';
+
+
+export default (o) => {
+    return html`
+        <div class="modal-dialog" role="document">
+            <div class="modal-content">
+                <div class="modal-header">
+                    <h5 class="modal-title" id="user-details-modal-label">${o.display_name}</h5>
+                    ${modal_header_close_button}
+                </div>
+                <div class="modal-body">
+                    ${renderAvatar(o.avatar_data)}
+                </div>
+                <div class="modal-footer">
+                    ${modal_close_button}
+                </div>
+            </div>
+        </div>
+    `;
+}

+ 100 - 0
src/modals/user-details.js

@@ -0,0 +1,100 @@
+import log from "@converse/headless/log";
+import tpl_user_details_modal from "../templates/user_details_modal.js";
+import { BootstrapModal } from "../converse-modal.js";
+import { __ } from '../i18n';
+import { _converse, api, converse } from "@converse/headless/converse-core";
+
+const u = converse.env.utils;
+
+
+const UserDetailsModal = BootstrapModal.extend({
+    id: "user-details-modal",
+
+    events: {
+        'click button.refresh-contact': 'refreshContact',
+        'click .fingerprint-trust .btn input': 'toggleDeviceTrust'
+    },
+
+    initialize () {
+        BootstrapModal.prototype.initialize.apply(this, arguments);
+        this.model.rosterContactAdded.then(() => this.registerContactEventHandlers());
+        this.listenTo(this.model, 'change', this.render);
+        this.registerContactEventHandlers();
+        /**
+            * Triggered once the UserDetailsModal has been initialized
+            * @event _converse#userDetailsModalInitialized
+            * @type { _converse.ChatBox }
+            * @example _converse.api.listen.on('userDetailsModalInitialized', chatbox => { ... });
+            */
+        api.trigger('userDetailsModalInitialized', this.model);
+    },
+
+    toHTML () {
+        const vcard = this.model?.vcard;
+        const vcard_json = vcard ? vcard.toJSON() : {};
+        return tpl_user_details_modal(Object.assign(
+            this.model.toJSON(),
+            vcard_json, {
+            '_converse': _converse,
+            'allow_contact_removal': api.settings.get('allow_contact_removal'),
+            'display_name': this.model.getDisplayName(),
+            'is_roster_contact': this.model.contact !== undefined,
+            'removeContact': ev => this.removeContact(ev),
+            'view': this,
+            'utils': u
+        }));
+    },
+
+    registerContactEventHandlers () {
+        if (this.model.contact !== undefined) {
+            this.listenTo(this.model.contact, 'change', this.render);
+            this.listenTo(this.model.contact.vcard, 'change', this.render);
+            this.model.contact.on('destroy', () => {
+                delete this.model.contact;
+                this.render();
+            });
+        }
+    },
+
+    async refreshContact (ev) {
+        if (ev && ev.preventDefault) { ev.preventDefault(); }
+        const refresh_icon = this.el.querySelector('.fa-refresh');
+        u.addClass('fa-spin', refresh_icon);
+        try {
+            await api.vcard.update(this.model.contact.vcard, true);
+        } catch (e) {
+            log.fatal(e);
+            this.alert(__('Sorry, something went wrong while trying to refresh'), 'danger');
+        }
+        u.removeClass('fa-spin', refresh_icon);
+    },
+
+    removeContact (ev) {
+        if (ev && ev.preventDefault) { ev.preventDefault(); }
+        if (!api.settings.get('allow_contact_removal')) { return; }
+        const result = confirm(__("Are you sure you want to remove this contact?"));
+        if (result === true) {
+            this.modal.hide();
+            // XXX: This is annoying but necessary to get tests to pass.
+            // The `dismissHandler` in bootstrap.native tries to
+            // reference the remove button after it's been cleared from
+            // the DOM, so we delay removing the contact to give it time.
+            setTimeout(() => {
+                this.model.contact.removeFromRoster(
+                    () => this.model.contact.destroy(),
+                    (err) => {
+                        log.error(err);
+                        api.alert('error', __('Error'), [
+                            __('Sorry, there was an error while trying to remove %1$s as a contact.',
+                            this.model.contact.getDisplayName())
+                        ]);
+                    }
+                );
+            }, 1);
+        }
+    },
+});
+
+_converse.UserDetailsModal = UserDetailsModal;
+
+export default UserDetailsModal;

+ 2 - 2
src/templates/chat_message.js

@@ -16,12 +16,12 @@ export default (o) => {
             <!-- Anchor to allow us to scroll the message into view -->
             <a id="${o.msgid}"></a>
 
-            ${ o.shouldShowAvatar() ? renderAvatar(o.getAvatarData()) : '' }
+            <a class="show-msg-author-modal" @click=${o.showUserModal}>${ o.shouldShowAvatar() ? renderAvatar(o.getAvatarData()) : '' }</a>
             <div class="chat-msg__content chat-msg__content--${o.sender} ${o.is_me_message ? 'chat-msg__content--action' : ''}">
 
                 ${ !o.is_me_message ? html`
                     <span class="chat-msg__heading">
-                        <span class="chat-msg__author">${o.username}</span>
+                        <span class="chat-msg__author"><a class="show-msg-author-modal" @click=${o.showUserModal}>${o.username}</a></span>
                         ${ o.renderAvatarByline() }
                         ${ o.is_encrypted ? html`<span class="fa fa-lock"></span>` : '' }
                     </span>` : '' }

+ 7 - 12
src/templates/chatbox_head.js

@@ -1,26 +1,21 @@
+import { _converse } from '@converse/headless/converse-core';
 import { html } from "lit-html";
-import { __ } from '../i18n';
+import { renderAvatar } from './directives/avatar.js';
 import { until } from 'lit-html/directives/until.js';
-import avatar from "./avatar.js";
 
 
 export default (o) => {
-    const i18n_profile = __('The User\'s Profile Image');
-    const avatar_data = {
-        'alt_text': i18n_profile,
-        'extra_classes': '',
-        'height': 40,
-        'width': 40,
-    }
     const tpl_standalone_btns = (o) => o.standalone_btns.reverse().map(b => until(b, ''));
 
+    const avatar = html`<span class="mr-2">${renderAvatar(o.avatar_data)}</span>`;
+
     return html`
         <div class="chatbox-title ${ o.status ? '' :  "chatbox-title--no-desc"}">
             <div class="chatbox-title--row">
-                ${ (!o._converse.api.settings.get("singleton")) ? html`<div class="chatbox-navback"><i class="fa fa-arrow-left"></i></div>` : '' }
-                ${ (o.type !== o._converse.HEADLINES_TYPE) ? html`<span class="mr-2">${avatar(Object.assign({}, o, avatar_data))}</span>` : '' }
+                ${ (!_converse.api.settings.get("singleton")) ? html`<div class="chatbox-navback"><i class="fa fa-arrow-left"></i></div>` : '' }
+                ${ (o.type !== _converse.HEADLINES_TYPE) ? html`<a class="show-msg-author-modal" @click=${o.showUserDetailsModal}>${ avatar }</a>` : '' }
                 <div class="chatbox-title__text" title="${o.jid}">
-                    ${ o.url ? html`<a href="${o.url}" target="_blank" rel="noopener" class="user">${o.display_name}</a>` : o.display_name}
+                    ${ (o.type !== _converse.HEADLINES_TYPE) ? html`<a class="user show-msg-author-modal" @click=${o.showUserDetailsModal}>${ o.display_name }</a>` : o.display_name }
                 </div>
             </div>
             <div class="chatbox-title__buttons row no-gutters">