Allow user modals to be opened from message headings

This commit is contained in:
JC Brand 2020-12-01 16:31:57 +01:00
parent 5a3aaeb056
commit 34cba68432
11 changed files with 234 additions and 124 deletions

View File

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

View File

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

View File

@ -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) { showMessageVersionsModal (ev) {
ev.preventDefault(); ev.preventDefault();
if (this.message_versions_modal === undefined) { if (this.message_versions_modal === undefined) {

View File

@ -3,24 +3,23 @@
* @copyright 2020, the Converse.js contributors * @copyright 2020, the Converse.js contributors
* @license Mozilla Public License (MPLv2) * @license Mozilla Public License (MPLv2)
*/ */
import "./components/chat_content.js"; import './components/chat_content.js';
import "./components/help_messages.js"; import './components/help_messages.js';
import "./components/toolbar.js"; import './components/toolbar.js';
import "converse-chatboxviews"; import 'converse-chatboxviews';
import "converse-modal"; import 'converse-modal';
import log from "@converse/headless/log"; import log from '@converse/headless/log';
import tpl_chatbox from "templates/chatbox.js"; import tpl_chatbox from 'templates/chatbox.js';
import tpl_chatbox_head from "templates/chatbox_head.js"; import tpl_chatbox_head from 'templates/chatbox_head.js';
import tpl_chatbox_message_form from "templates/chatbox_message_form.js"; import tpl_chatbox_message_form from 'templates/chatbox_message_form.js';
import tpl_spinner from "templates/spinner.js"; import tpl_spinner from 'templates/spinner.js';
import tpl_toolbar from "templates/toolbar.js"; import tpl_toolbar from 'templates/toolbar.js';
import tpl_user_details_modal from "templates/user_details_modal.js"; import UserDetailsModal from 'modals/user-details.js';
import { BootstrapModal } from "./converse-modal.js";
import { View } from '@converse/skeletor/src/view.js'; import { View } from '@converse/skeletor/src/view.js';
import { __ } from './i18n'; import { __ } from './i18n';
import { _converse, api, converse } from "@converse/headless/converse-core"; import { _converse, api, converse } from '@converse/headless/converse-core';
import { debounce } from "lodash-es"; import { debounce } from 'lodash-es';
import { html, render } from "lit-html"; import { html, render } from 'lit-html';
const { Strophe, dayjs } = converse.env; const { Strophe, dayjs } = converse.env;
@ -240,7 +239,7 @@ export const ChatBoxView = View.extend({
showUserDetailsModal (ev) { showUserDetailsModal (ev) {
ev.preventDefault(); ev.preventDefault();
if (this.user_details_modal === undefined) { 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); this.user_details_modal.show(ev);
}, },
@ -283,17 +282,24 @@ export const ChatBoxView = View.extend({
async generateHeadingTemplate () { async generateHeadingTemplate () {
const vcard = this.model?.vcard; const vcard = this.model?.vcard;
const vcard_json = vcard ? vcard.toJSON() : {}; 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 heading_btns = await this.getHeadingButtons();
const standalone_btns = heading_btns.filter(b => b.standalone); const standalone_btns = heading_btns.filter(b => b.standalone);
const dropdown_btns = heading_btns.filter(b => !b.standalone); const dropdown_btns = heading_btns.filter(b => !b.standalone);
return tpl_chatbox_head( return tpl_chatbox_head(
Object.assign( Object.assign(
vcard_json,
this.model.toJSON(), { this.model.toJSON(), {
'_converse': _converse, avatar_data,
'display_name': this.model.getDisplayName(),
'dropdown_btns': dropdown_btns.map(b => this.getHeadingDropdownItem(b)), 'dropdown_btns': dropdown_btns.map(b => this.getHeadingDropdownItem(b)),
'showUserDetailsModal': ev => this.showUserDetailsModal(ev),
'standalone_btns': standalone_btns.map(b => this.getHeadingStandaloneButton(b)), '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.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', () => { api.listen.on('chatBoxViewsInitialized', () => {
const views = _converse.chatboxviews; const views = _converse.chatboxviews;
_converse.chatboxes.on('add', async item => { _converse.chatboxes.on('add', async item => {

View File

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

View File

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

47
src/modals/occupant.js Normal file
View File

@ -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;

View File

@ -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
src/modals/user-details.js Normal file
View File

@ -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;

View File

@ -16,12 +16,12 @@ export default (o) => {
<!-- Anchor to allow us to scroll the message into view --> <!-- Anchor to allow us to scroll the message into view -->
<a id="${o.msgid}"></a> <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' : ''}"> <div class="chat-msg__content chat-msg__content--${o.sender} ${o.is_me_message ? 'chat-msg__content--action' : ''}">
${ !o.is_me_message ? html` ${ !o.is_me_message ? html`
<span class="chat-msg__heading"> <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.renderAvatarByline() }
${ o.is_encrypted ? html`<span class="fa fa-lock"></span>` : '' } ${ o.is_encrypted ? html`<span class="fa fa-lock"></span>` : '' }
</span>` : '' } </span>` : '' }

View File

@ -1,26 +1,21 @@
import { _converse } from '@converse/headless/converse-core';
import { html } from "lit-html"; import { html } from "lit-html";
import { __ } from '../i18n'; import { renderAvatar } from './directives/avatar.js';
import { until } from 'lit-html/directives/until.js'; import { until } from 'lit-html/directives/until.js';
import avatar from "./avatar.js";
export default (o) => { 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 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` return html`
<div class="chatbox-title ${ o.status ? '' : "chatbox-title--no-desc"}"> <div class="chatbox-title ${ o.status ? '' : "chatbox-title--no-desc"}">
<div class="chatbox-title--row"> <div class="chatbox-title--row">
${ (!o._converse.api.settings.get("singleton")) ? html`<div class="chatbox-navback"><i class="fa fa-arrow-left"></i></div>` : '' } ${ (!_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>` : '' } ${ (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}"> <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> </div>
<div class="chatbox-title__buttons row no-gutters"> <div class="chatbox-title__buttons row no-gutters">