Componentize the chat headings

This commit is contained in:
JC Brand 2021-02-11 22:32:55 +01:00
parent 906fa93812
commit a8a2bb4681
15 changed files with 459 additions and 390 deletions

View File

@ -359,7 +359,7 @@
color: white;
&.muc-bottom-panel--muted {
height: 8em;
height: 4em;
width: 100%;
}

View File

@ -13,8 +13,7 @@ async function openModtools (_converse, view) {
const enter = { 'target': textarea, 'preventDefault': function preventDefault () {}, 'keyCode': 13 };
const bottom_panel = view.querySelector('converse-muc-bottom-panel');
bottom_panel.onKeyDown(enter);
await u.waitUntil(() => view.showModeratorToolsModal.calls.count());
const modal = _converse.api.modal.get('converse-modtools-modal');
const modal = await u.waitUntil(() => _converse.api.modal.get('converse-modtools-modal'));
await u.waitUntil(() => u.isVisible(modal.el), 1000);
return modal;
}
@ -24,7 +23,6 @@ describe("The groupchat moderator tool", function () {
it("allows you to set affiliations and roles",
mock.initConverse([], {}, async function (done, _converse) {
spyOn(_converse.ChatRoomView.prototype, 'showModeratorToolsModal').and.callThrough();
const muc_jid = 'lounge@montague.lit';
let members = [
@ -143,7 +141,6 @@ describe("The groupchat moderator tool", function () {
it("allows you to filter affiliation search results",
mock.initConverse([], {}, async function (done, _converse) {
spyOn(_converse.ChatRoomView.prototype, 'showModeratorToolsModal').and.callThrough();
const muc_jid = 'lounge@montague.lit';
const members = [
{'jid': 'hag66@shakespeare.lit', 'nick': 'witch', 'affiliation': 'member'},
@ -197,11 +194,9 @@ describe("The groupchat moderator tool", function () {
it("allows you to filter role search results",
mock.initConverse([], {}, async function (done, _converse) {
spyOn(_converse.ChatRoomView.prototype, 'showModeratorToolsModal').and.callThrough();
const muc_jid = 'lounge@montague.lit';
await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo', []);
const view = _converse.chatboxviews.get(muc_jid);
_converse.connection._dataRecv(mock.createRequest(
$pres({to: _converse.jid, from: `${muc_jid}/nomorenicks`})
.c('x', {xmlns: Strophe.NS.MUC_USER})
@ -263,9 +258,8 @@ describe("The groupchat moderator tool", function () {
const enter = { 'target': textarea, 'preventDefault': function preventDefault () {}, 'keyCode': 13 };
const bottom_panel = view.querySelector('converse-muc-bottom-panel');
bottom_panel.onKeyDown(enter);
await u.waitUntil(() => view.showModeratorToolsModal.calls.count());
const modal = _converse.api.modal.get('converse-modtools-modal');
const modal = await u.waitUntil(() => _converse.api.modal.get('converse-modtools-modal'));
await u.waitUntil(() => u.isVisible(modal.el), 1000);
const tab = modal.el.querySelector('#roles-tab');
@ -307,7 +301,6 @@ describe("The groupchat moderator tool", function () {
it("shows an error message if a particular affiliation list may not be retrieved",
mock.initConverse([], {}, async function (done, _converse) {
spyOn(_converse.ChatRoomView.prototype, 'showModeratorToolsModal').and.callThrough();
const muc_jid = 'lounge@montague.lit';
const members = [
{'jid': 'hag66@shakespeare.lit', 'nick': 'witch', 'affiliation': 'member'},
@ -357,7 +350,6 @@ describe("The groupchat moderator tool", function () {
it("shows an error message if a particular affiliation may not be set",
mock.initConverse([], {}, async function (done, _converse) {
spyOn(_converse.ChatRoomView.prototype, 'showModeratorToolsModal').and.callThrough();
const muc_jid = 'lounge@montague.lit';
const members = [
{'jid': 'gower@shakespeare.lit', 'nick': 'gower', 'affiliation': 'member'},
@ -422,7 +414,6 @@ describe("The groupchat moderator tool", function () {
it("doesn't allow admins to make more admins",
mock.initConverse([], {}, async function (done, _converse) {
spyOn(_converse.ChatRoomView.prototype, 'showModeratorToolsModal').and.callThrough();
const muc_jid = 'lounge@montague.lit';
const members = [
{'jid': 'hag66@shakespeare.lit', 'nick': 'witch', 'affiliation': 'member'},
@ -457,7 +448,6 @@ describe("The groupchat moderator tool", function () {
it("lets the assignable affiliations and roles be configured via modtools_disable_assign",
mock.initConverse([], {}, async function (done, _converse) {
spyOn(_converse.ChatRoomView.prototype, 'showModeratorToolsModal').and.callThrough();
const muc_jid = 'lounge@montague.lit';
const members = [{'jid': 'romeo@montague.lit', 'nick': 'romeo', 'affiliation': 'owner'}];
await mock.openAndEnterChatRoom(_converse, muc_jid, 'romeo', [], members);
@ -467,9 +457,8 @@ describe("The groupchat moderator tool", function () {
const enter = { 'target': textarea, 'preventDefault': function preventDefault () {}, 'keyCode': 13 };
const bottom_panel = view.querySelector('converse-muc-bottom-panel');
bottom_panel.onKeyDown(enter);
await u.waitUntil(() => view.showModeratorToolsModal.calls.count());
const modal = _converse.api.modal.get('converse-modtools-modal');
const modal = await u.waitUntil(() => _converse.api.modal.get('converse-modtools-modal'));
const occupant = view.model.occupants.findWhere({'jid': _converse.bare_jid});
expect(modal.getAssignableAffiliations(occupant)).toEqual(['owner', 'admin', 'member', 'outcast', 'none']);

View File

@ -1976,8 +1976,7 @@ describe("Groupchats", function () {
const view = _converse.chatboxviews.get('lounge@montague.lit');
expect(view.model.getOwnAffiliation()).toBe('owner');
expect(view.model.features.get('open')).toBe(false);
expect(view.querySelector('.open-invite-modal')).not.toBe(null);
await u.waitUntil(() => view.querySelector('.open-invite-modal'));
// Members can't invite if the room isn't open
view.model.getOwnOccupant().set('affiliation', 'member');
@ -2474,7 +2473,7 @@ describe("Groupchats", function () {
expect(view.model.features.get('temporary')).toBe(true);
expect(view.model.features.get('unmoderated')).toBe(true);
expect(view.model.features.get('unsecured')).toBe(false);
expect(view.querySelector('.chatbox-title__text').textContent.trim()).toBe('Room');
await u.waitUntil(() => view.querySelector('.chatbox-title__text').textContent.trim() === 'Room');
view.querySelector('.configure-chatroom-button').click();

View File

@ -76,36 +76,11 @@ export default class ChatBottomPanel extends ElementView {
}
emitFocused (ev) {
const chatview = _converse.chatboxviews.get(this.getAttribute('jid'));
if (chatview.contains(document.activeElement) || chatview.contains(ev.relatedTarget)) {
// Something else in this chatbox was already focused
return;
}
/**
* Triggered when the focus has been moved to a particular chat.
* @event _converse#chatBoxFocused
* @type { _converse.ChatBoxView | _converse.ChatRoomView }
* @example _converse.api.listen.on('chatBoxFocused', (view, event) => { ... });
*/
api.trigger('chatBoxFocused', this, ev);
_converse.chatboxviews.get(this.getAttribute('jid'))?.emitFocused(ev);
}
emitBlurred (ev) {
const chatview = _converse.chatboxviews.get(this.getAttribute('jid'));
if (!chatview) {
return;
}
if (chatview.contains(document.activeElement) || chatview.contains(ev.relatedTarget)) {
// Something else in this chatbox is still focused
return;
}
/**
* Triggered when the focus has been removed from a particular chat.
* @event _converse#chatBoxBlurred
* @type { _converse.ChatBoxView | _converse.ChatRoomView }
* @example _converse.api.listen.on('chatBoxBlurred', (view, event) => { ... });
*/
api.trigger('chatBoxBlurred', this, ev);
_converse.chatboxviews.get(this.getAttribute('jid'))?.emitBlurred(ev);
}
getToolbarOptions () { // eslint-disable-line class-methods-use-this

View File

@ -0,0 +1,124 @@
import UserDetailsModal from 'modals/user-details.js';
import debounce from 'lodash/debounce';
import tpl_chatbox_head from 'templates/chatbox_head.js';
import { ElementView } from '@converse/skeletor/src/element.js';
import { __ } from 'i18n';
import { _converse, api } from "@converse/headless/core";
import { getHeadingDropdownItem, getHeadingStandaloneButton } from 'plugins/chatview/utils.js';
import { render } from 'lit-html';
export default class ChatHeading extends ElementView {
async render () {
const tpl = await this.generateHeadingTemplate();
render(tpl, this);
}
connectedCallback () {
super.connectedCallback();
this.model = _converse.chatboxes.get(this.getAttribute('jid'));
this.debouncedRender = debounce(this.render, 100);
this.listenTo(this.model, 'vcard:change', this.debouncedRender);
if (this.model.contact) {
this.listenTo(this.model.contact, 'destroy', this.debouncedRender);
}
this.model.rosterContactAdded?.then(() => {
this.listenTo(this.model.contact, 'change:nickname', this.debouncedRender);
this.debouncedRender();
});
this.render();
}
showUserDetailsModal (ev) {
ev.preventDefault();
api.modal.show(UserDetailsModal, { model: this.model }, ev);
}
close () {
_converse.chatboxviews.get(this.getAttribute('jid'))?.close();
}
/**
* Returns a list of objects which represent buttons for the chat's header.
* @async
* @emits _converse#getHeadingButtons
*/
getHeadingButtons () {
const buttons = [
{
'a_class': 'show-user-details-modal',
'handler': ev => this.showUserDetailsModal(ev),
'i18n_text': __('Details'),
'i18n_title': __('See more information about this person'),
'icon_class': 'fa-id-card',
'name': 'details',
'standalone': api.settings.get('view_mode') === 'overlayed'
}
];
if (!api.settings.get('singleton')) {
buttons.push({
'a_class': 'close-chatbox-button',
'handler': ev => this.close(ev),
'i18n_text': __('Close'),
'i18n_title': __('Close and end this conversation'),
'icon_class': 'fa-times',
'name': 'close',
'standalone': api.settings.get('view_mode') === 'overlayed'
});
}
/**
* *Hook* which allows plugins to add more buttons to a chat's heading.
* @event _converse#getHeadingButtons
* @example
* api.listen.on('getHeadingButtons', (view, buttons) => {
* buttons.push({
* 'i18n_title': __('Foo'),
* 'i18n_text': __('Foo Bar'),
* 'handler': ev => alert('Foo!'),
* 'a_class': 'toggle-foo',
* 'icon_class': 'fa-foo',
* 'name': 'foo'
* });
* return buttons;
* });
*/
const chatview = _converse.chatboxviews.get(this.getAttribute('jid'));
if (chatview) {
return _converse.api.hook('getHeadingButtons', chatview, buttons);
} else {
return buttons; // Happens during tests
}
}
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(this.model.toJSON(), {
avatar_data,
'display_name': this.model.getDisplayName(),
'dropdown_btns': dropdown_btns.map(b => getHeadingDropdownItem(b)),
'showUserDetailsModal': ev => this.showUserDetailsModal(ev),
'standalone_btns': standalone_btns.map(b => getHeadingStandaloneButton(b))
})
);
}
}
api.elements.define('converse-chat-heading', ChatHeading);

View File

@ -0,0 +1,23 @@
import { html } from 'lit-html';
export async function getHeadingDropdownItem (promise_or_data) {
const data = await promise_or_data;
return html`
<a href="#" class="dropdown-item ${data.a_class}" @click=${data.handler} title="${data.i18n_title}"
><i class="fa ${data.icon_class}"></i>${data.i18n_text}</a
>
`;
}
export async function getHeadingStandaloneButton (promise_or_data) {
const data = await promise_or_data;
return html`
<a
href="#"
class="chatbox-btn ${data.a_class} fa ${data.icon_class}"
@click=${data.handler}
title="${data.i18n_title}"
></a>
`;
}

View File

@ -1,8 +1,7 @@
import 'plugins/chatview/heading.js';
import 'plugins/chatview/bottom_panel.js';
import BaseChatView from 'shared/chat/baseview.js';
import UserDetailsModal from 'modals/user-details.js';
import tpl_chatbox from 'templates/chatbox.js';
import tpl_chatbox_head from 'templates/chatbox_head.js';
import { __ } from 'i18n';
import { _converse, api, converse } from '@converse/headless/core';
import { render } from 'lit-html';
@ -36,19 +35,7 @@ export default class ChatView extends BaseChatView {
this.listenTo(_converse, 'windowStateChanged', this.onWindowStateChanged);
this.listenTo(this.model, 'change:hidden', () => !this.model.get('hidden') && this.afterShown());
this.listenTo(this.model, 'change:status', this.onStatusMessageChanged);
this.listenTo(this.model, 'vcard:change', this.renderHeading);
this.listenTo(this.model.messages, 'change:correcting', this.onMessageCorrecting);
if (this.model.contact) {
this.listenTo(this.model.contact, 'destroy', this.renderHeading);
}
if (this.model.rosterContactAdded) {
this.model.rosterContactAdded.then(() => {
this.listenTo(this.model.contact, 'change:nickname', this.renderHeading);
this.renderHeading();
});
}
this.listenTo(this.model.presence, 'change:show', this.onPresenceChanged);
this.render();
@ -74,7 +61,6 @@ export default class ChatView extends BaseChatView {
render(result, this);
this.content = this.querySelector('.chat-content');
this.help_container = this.querySelector('.chat-content__help');
this.renderHeading();
return this;
}
@ -93,87 +79,6 @@ export default class ChatView extends BaseChatView {
this.hide();
}
showUserDetailsModal (ev) {
ev.preventDefault();
api.modal.show(UserDetailsModal, { model: this.model }, ev);
}
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(this.model.toJSON(), {
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))
})
);
}
/**
* Returns a list of objects which represent buttons for the chat's header.
* @async
* @emits _converse#getHeadingButtons
* @private
* @method _converse.ChatBoxView#getHeadingButtons
*/
getHeadingButtons () {
const buttons = [
{
'a_class': 'show-user-details-modal',
'handler': ev => this.showUserDetailsModal(ev),
'i18n_text': __('Details'),
'i18n_title': __('See more information about this person'),
'icon_class': 'fa-id-card',
'name': 'details',
'standalone': api.settings.get('view_mode') === 'overlayed'
}
];
if (!api.settings.get('singleton')) {
buttons.push({
'a_class': 'close-chatbox-button',
'handler': ev => this.close(ev),
'i18n_text': __('Close'),
'i18n_title': __('Close and end this conversation'),
'icon_class': 'fa-times',
'name': 'close',
'standalone': api.settings.get('view_mode') === 'overlayed'
});
}
/**
* *Hook* which allows plugins to add more buttons to a chat's heading.
* @event _converse#getHeadingButtons
* @example
* api.listen.on('getHeadingButtons', (view, buttons) => {
* buttons.push({
* 'i18n_title': __('Foo'),
* 'i18n_text': __('Foo Bar'),
* 'handler': ev => alert('Foo!'),
* 'a_class': 'toggle-foo',
* 'icon_class': 'fa-foo',
* 'name': 'foo'
* });
* return buttons;
* });
*/
return _converse.api.hook('getHeadingButtons', this, buttons);
}
/**
* Given a message element, determine wether it should be
* marked as a followup message to the previous element.

View File

@ -0,0 +1,54 @@
import ChatHeading from 'plugins/chatview/heading.js';
import tpl_chat_head from './templates/chat-head.js';
import { __ } from 'i18n';
import { _converse, api } from "@converse/headless/core";
import { getHeadingDropdownItem, getHeadingStandaloneButton } from 'plugins/chatview/utils.js';
export default class HeadlinesHeading extends ChatHeading {
async connectedCallback () {
super.connectedCallback();
this.model = _converse.chatboxes.get(this.getAttribute('jid'));
await this.model.initialized;
this.render();
}
async generateHeadingTemplate () {
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_chat_head(
Object.assign(this.model.toJSON(), {
'display_name': this.model.getDisplayName(),
'dropdown_btns': dropdown_btns.map(b => getHeadingDropdownItem(b)),
'standalone_btns': standalone_btns.map(b => getHeadingStandaloneButton(b))
})
);
}
/**
* Returns a list of objects which represent buttons for the headlines header.
* @async
* @emits _converse#getHeadingButtons
* @method HeadlinesHeading#getHeadingButtons
*/
getHeadingButtons () {
const buttons = [];
if (!api.settings.get('singleton')) {
buttons.push({
'a_class': 'close-chatbox-button',
'handler': ev => this.close(ev),
'i18n_text': __('Close'),
'i18n_title': __('Close these announcements'),
'icon_class': 'fa-times',
'name': 'close',
'standalone': api.settings.get('view_mode') === 'overlayed'
});
}
return _converse.api.hook('getHeadingButtons', this, buttons);
}
}
api.elements.define('converse-headlines-heading', HeadlinesHeading);

View File

@ -0,0 +1,19 @@
import '../heading.js';
import { html } from "lit-html";
export default (o) => html`
<div class="flyout box-flyout">
<converse-dragresize></converse-dragresize>
<converse-headlines-heading jid="${o.jid}" class="chat-head chat-head-chatbox row no-gutters"></converse-headlines-heading>
<div class="chat-body">
<div class="chat-content ${ o.show_send_button ? 'chat-content-sendbutton' : '' }" aria-live="polite">
<converse-chat-content
class="chat-content__messages"
jid="${o.jid}"
@scroll=${o.markScrolled}></converse-chat-content>
<div class="chat-content__help"></div>
</div>
</div>
</div>
`;

View File

@ -1,7 +1,5 @@
import BaseChatView from 'shared/chat/baseview.js';
import tpl_chatbox from 'templates/chatbox.js';
import tpl_chat_head from './templates/chat-head.js';
import { __ } from 'i18n';
import tpl_headlines from './templates/headlines.js';
import { _converse, api } from '@converse/headless/core';
import { render } from 'lit-html';
@ -47,7 +45,7 @@ class HeadlinesView extends BaseChatView {
render () {
this.setAttribute('id', this.model.get('box_id'));
const result = tpl_chatbox(
const result = tpl_headlines(
Object.assign(this.model.toJSON(), {
show_send_button: false,
show_toolbar: false,
@ -55,7 +53,6 @@ class HeadlinesView extends BaseChatView {
);
render(result, this);
this.content = this.querySelector('.chat-content');
this.renderHeading();
return this;
}
@ -76,47 +73,6 @@ class HeadlinesView extends BaseChatView {
return [];
}
async generateHeadingTemplate () {
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_chat_head(
Object.assign(this.model.toJSON(), {
'display_name': this.model.getDisplayName(),
'dropdown_btns': dropdown_btns.map(b => this.getHeadingDropdownItem(b)),
'standalone_btns': standalone_btns.map(b => this.getHeadingStandaloneButton(b))
})
);
}
/**
* Returns a list of objects which represent buttons for the headlines header.
* @async
* @emits _converse#getHeadingButtons
* @private
* @method _converse.HeadlinesBoxView#getHeadingButtons
*/
getHeadingButtons () {
const buttons = [];
if (!api.settings.get('singleton')) {
buttons.push({
'a_class': 'close-chatbox-button',
'handler': ev => this.close(ev),
'i18n_text': __('Close'),
'i18n_title': __('Close these announcements'),
'icon_class': 'fa-times',
'name': 'close',
'standalone': api.settings.get('view_mode') === 'overlayed'
});
}
return _converse.api.hook('getHeadingButtons', this, buttons);
}
// Override to avoid the methods in converse-chatview
renderMessageForm () { // eslint-disable-line class-methods-use-this
return;
}
afterShown () { // eslint-disable-line class-methods-use-this
return;
}

View File

@ -0,0 +1,191 @@
import ChatHeading from 'plugins/chatview/heading.js';
import MUCInviteModal from 'modals/muc-invite.js';
import RoomDetailsModal from 'modals/muc-details.js';
import debounce from 'lodash/debounce';
import tpl_muc_head from './templates/muc_head.js';
import { Model } from '@converse/skeletor/src/model.js';
import { __ } from 'i18n';
import { _converse, api, converse } from "@converse/headless/core";
import { getHeadingDropdownItem, getHeadingStandaloneButton } from 'plugins/chatview/utils.js';
export default class MUCHeading extends ChatHeading {
async connectedCallback () {
super.connectedCallback();
this.model = _converse.chatboxes.get(this.getAttribute('jid'));
this.debouncedRender = debounce(this.render, 100);
this.listenTo(this.model, 'change', this.debouncedRender);
const user_settings = await _converse.api.user.settings.getModel();
this.listenTo(user_settings, 'change:mucs_with_hidden_subject', this.debouncedRender);
await this.model.initialized;
this.listenTo(this.model.features, 'change:open', this.debouncedRender);
this.model.occupants.forEach(o => this.onOccupantAdded(o));
this.listenTo(this.model.occupants, 'add', this.onOccupantAdded);
this.listenTo(this.model.occupants, 'change:affiliation', this.onOccupantAffiliationChanged);
this.render();
}
onOccupantAdded (occupant) {
if (occupant.get('jid') === _converse.bare_jid) {
this.debouncedRender();
}
}
onOccupantAffiliationChanged (occupant) {
if (occupant.get('jid') === _converse.bare_jid) {
this.debouncedRender();
}
}
showRoomDetailsModal (ev) {
ev.preventDefault();
api.modal.show(RoomDetailsModal, { 'model': this.model }, ev);
}
showInviteModal (ev) {
ev.preventDefault();
api.modal.show(MUCInviteModal, { 'model': new Model(), 'chatroomview': this }, ev);
}
toggleTopic (ev) {
ev?.preventDefault?.();
this.model.toggleSubjectHiddenState();
}
getAndRenderConfigurationForm () {
_converse.chatboxviews.get(this.getAttribute('jid'))?.getAndRenderConfigurationForm();
}
showModeratorToolsModal () {
_converse.chatboxviews.get(this.getAttribute('jid'))?.showModeratorToolsModal();
}
destroy () {
_converse.chatboxviews.get(this.getAttribute('jid'))?.destroy();
}
/**
* Returns a list of objects which represent buttons for the groupchat header.
* @emits _converse#getHeadingButtons
*/
getHeadingButtons (subject_hidden) {
const buttons = [];
buttons.push({
'i18n_text': __('Details'),
'i18n_title': __('Show more information about this groupchat'),
'handler': ev => this.showRoomDetailsModal(ev),
'a_class': 'show-muc-details-modal',
'icon_class': 'fa-info-circle',
'name': 'details'
});
if (this.model.getOwnAffiliation() === 'owner') {
buttons.push({
'i18n_text': __('Configure'),
'i18n_title': __('Configure this groupchat'),
'handler': ev => this.getAndRenderConfigurationForm(ev),
'a_class': 'configure-chatroom-button',
'icon_class': 'fa-wrench',
'name': 'configure'
});
}
if (this.model.invitesAllowed()) {
buttons.push({
'i18n_text': __('Invite'),
'i18n_title': __('Invite someone to join this groupchat'),
'handler': ev => this.showInviteModal(ev),
'a_class': 'open-invite-modal',
'icon_class': 'fa-user-plus',
'name': 'invite'
});
}
const subject = this.model.get('subject');
if (subject && subject.text) {
buttons.push({
'i18n_text': subject_hidden ? __('Show topic') : __('Hide topic'),
'i18n_title': subject_hidden
? __('Show the topic message in the heading')
: __('Hide the topic in the heading'),
'handler': ev => this.toggleTopic(ev),
'a_class': 'hide-topic',
'icon_class': 'fa-minus-square',
'name': 'toggle-topic'
});
}
const conn_status = this.model.session.get('connection_status');
if (conn_status === converse.ROOMSTATUS.ENTERED) {
const allowed_commands = this.model.getAllowedCommands();
if (allowed_commands.includes('modtools')) {
buttons.push({
'i18n_text': __('Moderate'),
'i18n_title': __('Moderate this groupchat'),
'handler': () => this.showModeratorToolsModal(),
'a_class': 'moderate-chatroom-button',
'icon_class': 'fa-user-cog',
'name': 'moderate'
});
}
if (allowed_commands.includes('destroy')) {
buttons.push({
'i18n_text': __('Destroy'),
'i18n_title': __('Remove this groupchat'),
'handler': ev => this.destroy(ev),
'a_class': 'destroy-chatroom-button',
'icon_class': 'fa-trash',
'name': 'destroy'
});
}
}
if (!api.settings.get('singleton')) {
buttons.push({
'i18n_text': __('Leave'),
'i18n_title': __('Leave and close this groupchat'),
'handler': async ev => {
ev.stopPropagation();
const messages = [__('Are you sure you want to leave this groupchat?')];
const result = await api.confirm(__('Confirm'), messages);
result && this.close(ev);
},
'a_class': 'close-chatbox-button',
'standalone': api.settings.get('view_mode') === 'overlayed',
'icon_class': 'fa-sign-out-alt',
'name': 'signout'
});
}
const chatview = _converse.chatboxviews.get(this.getAttribute('jid'));
if (chatview) {
return _converse.api.hook('getHeadingButtons', chatview, buttons);
} else {
return buttons; // Happens during tests
}
}
/**
* Returns the groupchat heading TemplateResult to be rendered.
*/
async generateHeadingTemplate () {
const subject_hidden = await this.model.isSubjectHidden();
const heading_btns = await this.getHeadingButtons(subject_hidden);
const standalone_btns = heading_btns.filter(b => b.standalone);
const dropdown_btns = heading_btns.filter(b => !b.standalone);
return tpl_muc_head(
Object.assign(this.model.toJSON(), {
_converse,
subject_hidden,
'dropdown_btns': dropdown_btns.map(b => getHeadingDropdownItem(b)),
'standalone_btns': standalone_btns.map(b => getHeadingStandaloneButton(b)),
'title': this.model.getDisplayName()
})
);
}
}
api.elements.define('converse-muc-heading', MUCHeading);

View File

@ -1,14 +1,10 @@
import './bottom_panel.js';
import './config-form.js';
import './password-form.js';
import 'shared/autocomplete/index.js';
import BaseChatView from 'shared/chat/baseview.js';
import MUCInviteModal from 'modals/muc-invite.js';
import ModeratorToolsModal from 'modals/moderator-tools.js';
import RoomDetailsModal from 'modals/muc-details.js';
import log from '@converse/headless/log';
import tpl_muc from './templates/muc.js';
import tpl_muc_head from './templates/muc_head.js';
import tpl_muc_destroyed from './templates/muc_destroyed.js';
import tpl_muc_disconnect from './templates/muc_disconnect.js';
import tpl_muc_nickname_form from './templates/muc_nickname_form.js';
@ -16,7 +12,6 @@ import tpl_spinner from 'templates/spinner.js';
import { Model } from '@converse/skeletor/src/model.js';
import { __ } from 'i18n';
import { _converse, api, converse } from '@converse/headless/core';
import { debounce } from 'lodash-es';
import { render } from 'lit-html';
const { sizzle } = converse.env;
@ -53,14 +48,12 @@ export default class MUCView extends BaseChatView {
this.initDebounced();
this.listenTo(_converse, 'windowStateChanged', this.onWindowStateChanged);
this.listenTo(this.model, 'change', debounce(() => this.renderHeading(), 250));
this.listenTo(this.model, 'change:composing_spoiler', this.renderMessageForm);
this.listenTo(this.model, 'change:hidden', () => this.afterShown());
this.listenTo(this.model, 'change:hidden_occupants', this.onSidebarToggle);
this.listenTo(this.model, 'change:minimized', () => this.afterShown());
this.listenTo(this.model, 'configurationNeeded', this.getAndRenderConfigurationForm);
this.listenTo(this.model, 'show', this.show);
this.listenTo(this.model.features, 'change:open', this.renderHeading);
this.listenTo(this.model.messages, 'change:correcting', this.onMessageCorrecting);
this.listenTo(this.model.session, 'change:connection_status', this.renderAfterTransition);
@ -73,16 +66,9 @@ export default class MUCView extends BaseChatView {
// Need to be registered after render has been called.
this.listenTo(this.model, 'change:show_help_messages', this.renderHelpMessages);
this.listenTo(this.model.messages, 'add', this.onMessageAdded);
this.model.occupants.forEach(o => this.onOccupantAdded(o));
this.listenTo(this.model.occupants, 'add', this.onOccupantAdded);
this.listenTo(this.model.occupants, 'change:affiliation', this.onOccupantAffiliationChanged);
this.listenTo(this.model.occupants, 'change:show', this.showJoinOrLeaveNotification);
this.listenTo(this.model.occupants, 'remove', this.onOccupantRemoved);
// Register later due to await
const user_settings = await _converse.api.user.settings.getModel();
this.listenTo(user_settings, 'change:mucs_with_hidden_subject', this.renderHeading);
this.renderAfterTransition();
this.model.maybeShow();
this.scrollDown();
@ -95,7 +81,7 @@ export default class MUCView extends BaseChatView {
api.trigger('chatRoomViewInitialized', this);
}
async render () {
render () {
const sidebar_hidden = !this.shouldShowSidebar();
this.setAttribute('id', this.model.get('box_id'));
render(
@ -126,7 +112,6 @@ export default class MUCView extends BaseChatView {
// Render header as late as possible since it's async and we
// want the rest of the DOM elements to be available ASAP.
// Otherwise e.g. this.notifications is not yet defined when accessed elsewhere.
await this.renderHeading();
!this.model.get('hidden') && this.show();
}
@ -159,17 +144,6 @@ export default class MUCView extends BaseChatView {
.filter(line => this.model.getAllowedCommands().some(c => line.startsWith(c + '<', 9)));
}
/**
* Renders the MUC heading if any relevant attributes have changed.
* @private
* @method _converse.ChatRoomView#renderHeading
* @param { _converse.ChatRoom } [item]
*/
async renderHeading () {
const tpl = await this.generateHeadingTemplate();
render(tpl, this.querySelector('.chat-head-chatroom'));
}
onStartResizeOccupants (ev) {
this.resizing = true;
this.addEventListener('mousemove', this.onMouseMove);
@ -266,11 +240,6 @@ export default class MUCView extends BaseChatView {
modal.show();
}
showRoomDetailsModal (ev) {
ev.preventDefault();
api.modal.show(RoomDetailsModal, { 'model': this.model }, ev);
}
showChatStateNotification (message) {
if (message.get('sender') === 'me') {
return;
@ -289,139 +258,6 @@ export default class MUCView extends BaseChatView {
this.querySelector('.occupants')?.setVisibility();
}
onOccupantAffiliationChanged (occupant) {
if (occupant.get('jid') === _converse.bare_jid) {
this.renderHeading();
}
}
/**
* Returns a list of objects which represent buttons for the groupchat header.
* @emits _converse#getHeadingButtons
* @private
* @method _converse.ChatRoomView#getHeadingButtons
*/
getHeadingButtons (subject_hidden) {
const buttons = [];
buttons.push({
'i18n_text': __('Details'),
'i18n_title': __('Show more information about this groupchat'),
'handler': ev => this.showRoomDetailsModal(ev),
'a_class': 'show-muc-details-modal',
'icon_class': 'fa-info-circle',
'name': 'details'
});
if (this.model.getOwnAffiliation() === 'owner') {
buttons.push({
'i18n_text': __('Configure'),
'i18n_title': __('Configure this groupchat'),
'handler': ev => this.getAndRenderConfigurationForm(ev),
'a_class': 'configure-chatroom-button',
'icon_class': 'fa-wrench',
'name': 'configure'
});
}
if (this.model.invitesAllowed()) {
buttons.push({
'i18n_text': __('Invite'),
'i18n_title': __('Invite someone to join this groupchat'),
'handler': ev => this.showInviteModal(ev),
'a_class': 'open-invite-modal',
'icon_class': 'fa-user-plus',
'name': 'invite'
});
}
const subject = this.model.get('subject');
if (subject && subject.text) {
buttons.push({
'i18n_text': subject_hidden ? __('Show topic') : __('Hide topic'),
'i18n_title': subject_hidden
? __('Show the topic message in the heading')
: __('Hide the topic in the heading'),
'handler': ev => this.toggleTopic(ev),
'a_class': 'hide-topic',
'icon_class': 'fa-minus-square',
'name': 'toggle-topic'
});
}
const conn_status = this.model.session.get('connection_status');
if (conn_status === converse.ROOMSTATUS.ENTERED) {
const allowed_commands = this.model.getAllowedCommands();
if (allowed_commands.includes('modtools')) {
buttons.push({
'i18n_text': __('Moderate'),
'i18n_title': __('Moderate this groupchat'),
'handler': () => this.showModeratorToolsModal(),
'a_class': 'moderate-chatroom-button',
'icon_class': 'fa-user-cog',
'name': 'moderate'
});
}
if (allowed_commands.includes('destroy')) {
buttons.push({
'i18n_text': __('Destroy'),
'i18n_title': __('Remove this groupchat'),
'handler': ev => this.destroy(ev),
'a_class': 'destroy-chatroom-button',
'icon_class': 'fa-trash',
'name': 'destroy'
});
}
}
if (!api.settings.get('singleton')) {
buttons.push({
'i18n_text': __('Leave'),
'i18n_title': __('Leave and close this groupchat'),
'handler': async ev => {
ev.stopPropagation();
const messages = [__('Are you sure you want to leave this groupchat?')];
const result = await api.confirm(__('Confirm'), messages);
result && this.close(ev);
},
'a_class': 'close-chatbox-button',
'standalone': api.settings.get('view_mode') === 'overlayed',
'icon_class': 'fa-sign-out-alt',
'name': 'signout'
});
}
return _converse.api.hook('getHeadingButtons', this, buttons);
}
/**
* Returns the groupchat heading TemplateResult to be rendered.
* @private
* @method _converse.ChatRoomView#generateHeadingTemplate
*/
async generateHeadingTemplate () {
const subject_hidden = await this.model.isSubjectHidden();
const heading_btns = await this.getHeadingButtons(subject_hidden);
const standalone_btns = heading_btns.filter(b => b.standalone);
const dropdown_btns = heading_btns.filter(b => !b.standalone);
return tpl_muc_head(
Object.assign(this.model.toJSON(), {
_converse,
subject_hidden,
'dropdown_btns': dropdown_btns.map(b => this.getHeadingDropdownItem(b)),
'standalone_btns': standalone_btns.map(b => this.getHeadingStandaloneButton(b)),
'title': this.model.getDisplayName()
})
);
}
toggleTopic () {
this.model.toggleSubjectHiddenState();
}
showInviteModal (ev) {
ev.preventDefault();
api.modal.show(MUCInviteModal, { 'model': new Model(), 'chatroomview': this }, ev);
}
/**
* Callback method that gets called after the chat has become visible.
* @private
@ -660,11 +496,6 @@ export default class MUCView extends BaseChatView {
u.showElement(container);
}
onOccupantAdded (occupant) {
if (occupant.get('jid') === _converse.bare_jid) {
this.renderHeading();
}
}
/**
* Working backwards, get today's most recent join/leave notification

View File

@ -1,20 +1,22 @@
import '../heading.js';
import '../bottom_panel.js';
import { html } from "lit-html";
export default (o) => html`
<div class="flyout box-flyout">
<converse-dragresize></converse-dragresize>
<div class="chat-head chat-head-chatroom row no-gutters"></div>
<converse-muc-heading jid="${o.model.get('jid')}" class="chat-head chat-head-chatroom row no-gutters"></converse-muc-heading>
<div class="chat-body chatroom-body row no-gutters">
<div class="chat-area col">
<div class="chat-content ${ o.show_send_button ? 'chat-content-sendbutton' : '' }" aria-live="polite">
<converse-chat-content
class="chat-content__messages"
jid=${o.model.get('jid')}
jid="${o.model.get('jid')}"
@scroll=${o.markScrolled}></converse-chat-content>
<div class="chat-content__help"></div>
</div>
<converse-muc-bottom-panel jid=${o.model.get('jid')} class="bottom-panel"></converse-muc-bottom-panel>
<converse-muc-bottom-panel jid="${o.model.get('jid')}" class="bottom-panel"></converse-muc-bottom-panel>
</div>
<div class="disconnect-container hidden"></div>
<converse-muc-sidebar class="occupants col-md-3 col-4 ${o.sidebar_hidden ? 'hidden' : ''}"

View File

@ -14,11 +14,6 @@ export default class BaseChatView extends ElementView {
this.debouncedScrollDown = debounce(this.scrollDown, 100);
}
async renderHeading () {
const tpl = await this.generateHeadingTemplate();
render(tpl, this.querySelector('.chat-head-chatbox'));
}
renderHelpMessages () {
render(
html`
@ -34,18 +29,6 @@ export default class BaseChatView extends ElementView {
);
}
async getHeadingStandaloneButton (promise_or_data) { // eslint-disable-line class-methods-use-this
const data = await promise_or_data;
return html`
<a
href="#"
class="chatbox-btn ${data.a_class} fa ${data.icon_class}"
@click=${data.handler}
title="${data.i18n_title}"
></a>
`;
}
hideNewMessagesIndicator () {
const new_msgs_indicator = this.querySelector('.new-msgs-indicator');
if (new_msgs_indicator !== null) {
@ -77,6 +60,34 @@ export default class BaseChatView extends ElementView {
this.afterShown();
}
emitBlurred (ev) {
if (this.contains(document.activeElement) || this.contains(ev.relatedTarget)) {
// Something else in this chatbox is still focused
return;
}
/**
* Triggered when the focus has been removed from a particular chat.
* @event _converse#chatBoxBlurred
* @type { _converse.ChatBoxView | _converse.ChatRoomView }
* @example _converse.api.listen.on('chatBoxBlurred', (view, event) => { ... });
*/
api.trigger('chatBoxBlurred', this, ev);
}
emitFocused (ev) {
if (this.contains(ev.relatedTarget)) {
// Something else in this chatbox was already focused
return;
}
/**
* Triggered when the focus has been moved to a particular chat.
* @event _converse#chatBoxFocused
* @type { _converse.ChatBoxView | _converse.ChatRoomView }
* @example _converse.api.listen.on('chatBoxFocused', (view, event) => { ... });
*/
api.trigger('chatBoxFocused', this, ev);
}
/**
* Scroll to the previously saved scrollTop position, or scroll
* down if it wasn't set.
@ -123,16 +134,6 @@ export default class BaseChatView extends ElementView {
});
}
async getHeadingDropdownItem (promise_or_data) { // eslint-disable-line class-methods-use-this
const data = await promise_or_data;
return html`
<a href="#" class="dropdown-item ${data.a_class}" @click=${data.handler} title="${data.i18n_title}"
><i class="fa ${data.icon_class}"></i>${data.i18n_text}</a
>
`;
}
showNewMessagesIndicator () {
u.showElement(this.querySelector('.new-msgs-indicator'));
}

View File

@ -3,17 +3,17 @@ import { html } from "lit-html";
export default (o) => html`
<div class="flyout box-flyout">
<converse-dragresize></converse-dragresize>
<div class="chat-head chat-head-chatbox row no-gutters"></div>
<converse-chat-heading jid="${o.jid}" class="chat-head chat-head-chatbox row no-gutters"></converse-chat-heading>
<div class="chat-body">
<div class="chat-content ${ o.show_send_button ? 'chat-content-sendbutton' : '' }" aria-live="polite">
<converse-chat-content
class="chat-content__messages"
jid=${o.jid}
jid="${o.jid}"
@scroll=${o.markScrolled}></converse-chat-content>
<div class="chat-content__help"></div>
</div>
<converse-chat-bottom-panel jid=${o.jid} class="bottom-panel"> </converse-chat-bottom-panel>
<converse-chat-bottom-panel jid="${o.jid}" class="bottom-panel"> </converse-chat-bottom-panel>
</div>
</div>
`;