diff --git a/sass/_chatbox.scss b/sass/_chatbox.scss index 95a61e692..67160cf3f 100644 --- a/sass/_chatbox.scss +++ b/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); diff --git a/sass/_messages.scss b/sass/_messages.scss index 57a75bbc2..2880fadf9 100644 --- a/sass/_messages.scss +++ b/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; diff --git a/src/components/message.js b/src/components/message.js index 7071a370c..5bd6dfbe2 100644 --- a/src/components/message.js +++ b/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) { diff --git a/src/converse-chatview.js b/src/converse-chatview.js index ac2d334d1..d96d0a6a2 100644 --- a/src/converse-chatview.js +++ b/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 => { diff --git a/src/converse-muc-views.js b/src/converse-muc-views.js index d1d323b9b..a61a483d4 100644 --- a/src/converse-muc-views.js +++ b/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; diff --git a/src/converse-omemo.js b/src/converse-omemo.js index 333003082..a8beae4fb 100644 --- a/src/converse-omemo.js +++ b/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'; diff --git a/src/modals/occupant.js b/src/modals/occupant.js new file mode 100644 index 000000000..18e424119 --- /dev/null +++ b/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; diff --git a/src/modals/templates/occupant.js b/src/modals/templates/occupant.js new file mode 100644 index 000000000..292ca4da6 --- /dev/null +++ b/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` + + `; +} diff --git a/src/modals/user-details.js b/src/modals/user-details.js new file mode 100644 index 000000000..54be90fc9 --- /dev/null +++ b/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; diff --git a/src/templates/chat_message.js b/src/templates/chat_message.js index a0e9a875a..cc8a718f8 100644 --- a/src/templates/chat_message.js +++ b/src/templates/chat_message.js @@ -16,12 +16,12 @@ export default (o) => { - ${ o.shouldShowAvatar() ? renderAvatar(o.getAvatarData()) : '' } + ${ o.shouldShowAvatar() ? renderAvatar(o.getAvatarData()) : '' }
${ !o.is_me_message ? html` - ${o.username} + ${o.username} ${ o.renderAvatarByline() } ${ o.is_encrypted ? html`` : '' } ` : '' } diff --git a/src/templates/chatbox_head.js b/src/templates/chatbox_head.js index bd6ebca3e..418adf667 100644 --- a/src/templates/chatbox_head.js +++ b/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`${renderAvatar(o.avatar_data)}`; + return html`
- ${ (!o._converse.api.settings.get("singleton")) ? html`
` : '' } - ${ (o.type !== o._converse.HEADLINES_TYPE) ? html`${avatar(Object.assign({}, o, avatar_data))}` : '' } + ${ (!_converse.api.settings.get("singleton")) ? html`
` : '' } + ${ (o.type !== _converse.HEADLINES_TYPE) ? html`${ avatar }` : '' }
- ${ o.url ? html`${o.display_name}` : o.display_name} + ${ (o.type !== _converse.HEADLINES_TYPE) ? html`${ o.display_name }` : o.display_name }