Show MUC buttons in a dropdown menu

- Get rid of the ChatBoxHeading class
- Add support for showing standalone buttons in overlay viewmode
This commit is contained in:
JC Brand 2020-02-10 11:23:55 +01:00
parent 2a7773dce5
commit 3400acbfeb
27 changed files with 5991 additions and 783 deletions

5983
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -61,6 +61,10 @@
"@babel/preset-env": "^7.5.4",
"@converse/headless": "file:src/headless",
"@fortawesome/fontawesome-free": "5.9.0",
"@lit-element-bootstrap/button": "^1.0.0",
"@lit-element-bootstrap/button-group": "^1.0.0",
"@lit-element-bootstrap/dropdown": "^1.0.0-alpha.4",
"@open-wc/building-webpack": "^2.12.0",
"autoprefixer": "^9.6.1",
"babel-eslint": "^10.0.3",
"babel-loader": "^8.0.6",
@ -75,6 +79,7 @@
"eslint": "^6.3.0",
"eslint-plugin-lodash": "^5.1.0",
"exports-loader": "^0.7.0",
"fa-icons": "^0.1.9",
"fast-text-encoding": "^1.0.0",
"file-loader": "^4.0.0",
"html-webpack-plugin": "^3.2.0",
@ -109,6 +114,5 @@
"webpack-dev-server": "^3.8.0",
"webpack-merge": "^4.2.1",
"xss": "^1.0.6"
},
"dependencies": {}
}
}

View File

@ -41,7 +41,7 @@
color: #ffffff;
font-size: 100%;
margin: 0;
padding: 1rem;;
padding: 0;
position: relative;
&.chat-head-chatbox {
@ -54,22 +54,27 @@
.chat-head__desc {
color: var(--chat-head-color-lighten-50-percent);
font-size: 75%;
font-size: 80%;
font-size: var(--font-size-small);
margin: 0;
overflow: hidden;
padding: 0;
padding: 0.5rem 1rem 1rem 1rem;
text-overflow: ellipsis;
white-space: nowrap;
width: 100%;
}
.chatbox-title {
padding: 0.75rem 1rem 0 1rem;
display: flex;
flex-direction: row;
justify-content: space-between;
width: 100%;
}
.chatbox-title--no-desc {
padding: 0.75rem 1rem;
}
.chatbox-title--row {
display: flex;
flex-direction: row;
@ -137,7 +142,6 @@
display: flex;
flex-direction: column;
justify-content: space-between;
background-color: var(--chat-head-color);
box-shadow: 1px 3px 5px 3px rgba(0, 0, 0, 0.4);
z-index: 2;
overflow: hidden;
@ -175,7 +179,6 @@
display: flex;
flex-direction: column;
justify-content: space-between;
height: 100%;
background-color: var(--chat-head-color);
border-bottom-left-radius: var(--chatbox-border-radius);
border-bottom-right-radius: var(--chatbox-border-radius);
@ -426,8 +429,10 @@
#conversejs.converse-embedded,
#conversejs.converse-overlayed {
.chat-head {
.controlbox-head {
padding: 0.5em;
}
.chat-head {
border-top-left-radius: var(--chatbox-border-radius);
border-top-right-radius: var(--chatbox-border-radius);
@media screen and (max-height: $mobile-landscape-height) {
@ -474,6 +479,22 @@
}
}
}
.chat-body {
height: calc(100% - var(--overlayed-chat-head-height));
}
.chatbox-title {
padding: 0.5rem 0.75rem 0 0.75rem;
}
.chatbox-title--no-desc {
padding: 0.5rem 0.75rem;
}
converse-dropdown {
.btn--standalone {
padding: 0 0 0 0.5em;
}
}
}
}
@ -497,12 +518,6 @@
bottom: 0;
}
.chat-head {
.chat-head__desc {
font-size: 70%;
}
}
.chatbox {
margin: 0;
.box-flyout {
@ -590,7 +605,6 @@
}
.chatbox {
.box-flyout {
background-color: var(--chat-head-color);
box-shadow: none;
height: var(--fullpage-chat-height);
min-height: calc(var(--fullpage-chat-height) / 2);
@ -598,6 +612,7 @@
overflow: hidden;
}
.chat-body {
height: calc(100% - var(--fullpage-chat-head-height));
background-color: var(--chat-head-color);
}
.chat-title {

View File

@ -42,15 +42,28 @@
border-bottom: var(--chatroom-head-border-bottom);
.chat-head__desc {
color: var(--chatroom-head-description-color);
color: var(--chatroom-head-color);
display: var(--chatroom-head-description-display);
font-size: 70%;
margin-top: 3px;
border-left: var(--chatroom-head-description-border-left);
padding-left: var(--chatroom-head-description-padding-left);
a {
color: var(--chatroom-head-description-link-color);
}
&:hover {
button {
display: inline-block;
}
}
}
.chatbox-title {
.btn--transparent {
i {
color: var(--chatroom-head-color);
}
}
}
.chatbox-title__buttons {
background-color: var(--chatroom-head-bg-color);
}
a, a:visited, a:hover, a:not([href]):not([tabindex]) {
@ -73,6 +86,7 @@
display: var(--heading-display);
font-weight: var(--chatroom-head-title-font-weight);
padding-right: var(--chatroom-head-title-padding-right);
margin: auto 0;
.chatroom-jid {
font-size: var(--font-size-small);
}
@ -214,7 +228,6 @@
overflow-y: auto;
flex-basis: 0;
flex-grow: 1;
border-bottom: var(--occupants-border-bottom);
}
li {
cursor: default;
@ -467,10 +480,6 @@
.box-flyout {
width: 100%;
.chat-head__desc {
font-size: 70%;
}
.chatroom-body {
.chat-area {
&.full {

View File

@ -193,6 +193,20 @@ body.converse-fullscreen {
}
}
.dropdown-item {
padding: 0.5rem 1rem;
.fa {
margin-right: 0.75rem;
}
&:active, &.selected {
color: white !important;
background-color: var(--list-item-open-color);
.fa {
color: white !important;
}
}
}
.popover {
position: fixed;
}
@ -352,6 +366,10 @@ body.converse-fullscreen {
.fa, .far, .fas {
color: #fff;
margin-right: 0.5em;
&.only-icon {
margin-right: 0;
}
}
}
@ -541,6 +559,11 @@ body.converse-fullscreen {
}
}
.btn--transparent {
background: transparent;
border: none;
}
.btn-circle {
width: 30px;
height: 30px;

View File

@ -117,11 +117,8 @@ $mobile_portrait_length: 480px !default;
--chatroom-head-button-color: var(--chatroom-head-bg-color);
--chatroom-head-title-font-weight: normal;
--chatroom-head-title-padding-right: 0px;
--chatroom-head-description-color: var(--chatroom-head-bg-color-lighten-25-percent);
--chatroom-head-description-link-color: white;
--chatroom-head-description-display: block;
--chatroom-head-description-border-left: 0px;
--chatroom-head-description-padding-left: 0px;
--chatroom-head-border-bottom: 0px;
--chatroom-width: 500px;
--chatroom-correcting-color: #fadfd7; // lighten($red, 30%)

View File

@ -36,7 +36,7 @@
const view = _converse.chatboxviews.get(jid);
spyOn(view, 'renderBookmarkForm').and.callThrough();
spyOn(view, 'closeForm').and.callThrough();
await u.waitUntil(() => !_.isNull(view.el.querySelector('.toggle-bookmark')));
await u.waitUntil(() => view.el.querySelector('.toggle-bookmark') !== null);
let toggle = view.el.querySelector('.toggle-bookmark');
expect(toggle.title).toBe('Bookmark this groupchat');
toggle.click();
@ -216,8 +216,7 @@
);
await _converse.api.rooms.open(`lounge@montague.lit`);
const view = _converse.chatboxviews.get('lounge@montague.lit');
let bookmark_icon = await u.waitUntil(() => view.el.querySelector('.toggle-bookmark'));
expect(_.includes(bookmark_icon.classList, 'button-on')).toBeFalsy();
expect(view.el.querySelector('.chatbox-title__text .fa-bookmark')).toBe(null);
_converse.bookmarks.create({
'jid': view.model.get('jid'),
'autojoin': false,
@ -225,11 +224,9 @@
'nick': ' some1'
});
view.model.set('bookmarked', true);
bookmark_icon = await u.waitUntil(() => view.el.querySelector('.toggle-bookmark'));
expect(_.includes(bookmark_icon.classList, 'button-on')).toBeTruthy();
expect(view.el.querySelector('.chatbox-title__text .fa-bookmark')).not.toBe(null);
view.model.set('bookmarked', false);
bookmark_icon = await u.waitUntil(() => view.el.querySelector('.toggle-bookmark'));
expect(_.includes(bookmark_icon.classList, 'button-on')).toBeFalsy();
expect(view.el.querySelector('.chatbox-title__text .fa-bookmark')).toBe(null);
done();
}));
@ -256,14 +253,12 @@
expect(_converse.bookmarks.length).toBe(1);
await u.waitUntil(() => _converse.chatboxes.length >= 1);
expect(view.model.get('bookmarked')).toBeTruthy();
let bookmark_icon = await u.waitUntil(() => view.el.querySelector('.toggle-bookmark'));
expect(u.hasClass('button-on', bookmark_icon)).toBeTruthy();
expect(view.el.querySelector('.chatbox-title__text .fa-bookmark')).not.toBe(null);
spyOn(_converse.connection, 'getUniqueId').and.callThrough();
const bookmark_icon = view.el.querySelector('.toggle-bookmark');
bookmark_icon.click();
bookmark_icon = await u.waitUntil(() => view.el.querySelector('.toggle-bookmark'));
expect(view.toggleBookmark).toHaveBeenCalled();
expect(u.hasClass('button-on', bookmark_icon)).toBeFalsy();
expect(view.el.querySelector('.chatbox-title__text .fa-bookmark')).toBe(null);
expect(_converse.bookmarks.length).toBe(0);
// Check that an IQ stanza is sent out, containing no

View File

@ -364,9 +364,6 @@
expect(trimmedview.restore).toHaveBeenCalled();
expect(_converse.api.trigger).toHaveBeenCalledWith('chatBoxMaximized', jasmine.any(Object));
const toggle_el = sizzle('.toggle-chatbox-button', chatview.el).pop();
expect(u.hasClass('fa-minus', toggle_el)).toBeTruthy();
expect(u.hasClass('fa-plus', toggle_el)).toBeFalsy();
expect(chatview.model.get('minimized')).toBeFalsy();
done();
}));

View File

@ -149,6 +149,7 @@
const cbview = _converse.chatboxviews.get('controlbox');
await u.waitUntil(() => cbview.el.querySelectorAll(".open-headline").length);
const hlview = _converse.chatboxviews.get('notify.example.com');
await u.isVisible(hlview.el);
const close_el = hlview.el.querySelector('.close-chatbox-button');
close_el.click();
await u.waitUntil(() => cbview.el.querySelectorAll(".open-headline").length === 0);

View File

@ -1390,9 +1390,7 @@
}).up()
.c('status', {code: '110'});
_converse.connection._dataRecv(test_utils.createRequest(presence));
expect(u.isVisible(view.el.querySelector('.toggle-chatbox-button'))).toBeTruthy();
await u.waitUntil(() => !_.isNull(view.el.querySelector('.configure-chatroom-button')))
expect(u.isVisible(view.el.querySelector('.configure-chatroom-button'))).toBeTruthy();
await u.waitUntil(() => view.el.querySelector('.configure-chatroom-button') !== null);
view.el.querySelector('.configure-chatroom-button').click();
/* Check that an IQ is sent out, asking for the
@ -1949,13 +1947,14 @@
// Members can't invite if the room isn't open
view.model.getOwnOccupant().set('affiliation', 'member');
await u.waitUntil(() => view.el.querySelector('.open-invite-modal') === null);
view.model.features.set('open', 'true');
await u.waitUntil(() => view.el.querySelector('.open-invite-modal'));
view.el.querySelector('.open-invite-modal').click();
const modal = view.sidebar_view.muc_invite_modal;
const modal = view.muc_invite_modal;
await u.waitUntil(() => u.isVisible(modal.el), 1000)
expect(modal.el.querySelectorAll('#invitee_jids').length).toBe(1);
@ -2174,8 +2173,8 @@
_converse.connection._dataRecv(test_utils.createRequest(stanza));
const view = _converse.chatboxviews.get('jdev@conference.jabber.org');
await new Promise(resolve => view.model.once('change:subject', resolve));
expect(sizzle('.chat-event:last').pop().textContent.trim()).toBe('Topic set by ralphm');
expect(sizzle('.chat-topic:last').pop().textContent.trim()).toBe(text);
expect(sizzle('.chat-event:last', view.el).pop().textContent.trim()).toBe('Topic set by ralphm');
expect(view.el.querySelector('.chat-head__desc').textContent.trim()).toBe(text);
stanza = u.toStanza(
@ -2185,7 +2184,6 @@
</message>`);
_converse.connection._dataRecv(test_utils.createRequest(stanza));
await new Promise(resolve => view.once('messageInserted', resolve));
expect(sizzle('.chat-topic', view.el).length).toBe(1);
expect(sizzle('.chat-msg__subject', view.el).length).toBe(1);
expect(sizzle('.chat-msg__subject', view.el).pop().textContent.trim()).toBe('This is a message subject');
expect(sizzle('.chat-msg__text').length).toBe(1);
@ -2199,7 +2197,7 @@
</message>`);
_converse.connection._dataRecv(test_utils.createRequest(stanza));
await new Promise(resolve => view.model.once('change:subject', resolve));
expect(view.el.querySelector('.chat-head__desc').textContent.trim()).toBe("");
expect(view.el.querySelector('.chat-head__desc')).toBe(null);
expect(view.el.querySelector('.chat-info:last-child').textContent.trim()).toBe("Topic cleared by ralphm");
done();
}));
@ -2218,7 +2216,7 @@
'author': 'ralphm'
}});
expect(sizzle('.chat-event:last').pop().textContent.trim()).toBe('Topic set by ralphm');
expect(sizzle('.chat-topic:last').pop().textContent.trim()).toBe(subject);
expect(view.el.querySelector('.chat-head__desc').textContent.trim()).toBe(subject);
done();
}));
@ -2875,8 +2873,9 @@
spyOn(_converse.api, "trigger").and.callThrough();
spyOn(view.model, 'leave');
view.delegateEvents(); // We need to rebind all events otherwise our spy won't be called
spyOn(_converse.api, 'confirm').and.callFake(() => Promise.resolve(true));
view.el.querySelector('.close-chatbox-button').click();
expect(view.close).toHaveBeenCalled();
await u.waitUntil(() => view.close.calls.count());
expect(view.model.leave).toHaveBeenCalled();
await u.waitUntil(() => _converse.api.trigger.calls.count());
expect(_converse.api.trigger).toHaveBeenCalledWith('chatBoxClosed', jasmine.any(Object));
@ -4853,7 +4852,7 @@
await u.waitUntil(() => _converse.chatboxes.length > 1);
expect(sizzle('.chatroom', _converse.el).filter(u.isVisible).length).toBe(1); // There should now be an open chatroom
var view = _converse.chatboxviews.get('inverness@chat.shakespeare.lit');
expect(view.el.querySelector('.chat-head-chatroom').textContent.trim()).toBe("Macbeth's Castle");
expect(view.el.querySelector('.chatbox-title__text').textContent.trim()).toBe("Macbeth's Castle");
done();
}));

View File

@ -95,6 +95,7 @@
`</presence>`)
await u.waitUntil(() => modal.el.getAttribute('aria-hidden') === "true");
await u.waitUntil(() => !u.isVisible(modal.el));
cbview.el.querySelector('.change-status').click()
await u.waitUntil(() => modal.el.getAttribute('aria-hidden') === "false", 1000);
modal.el.querySelector('label[for="radio-busy"]').click(); // Change status to "dnd"

View File

@ -23,7 +23,6 @@
await u.waitUntil(() => _converse.chatboxes.length > 1);
const view = _converse.chatboxviews.get(contact_jid);
let show_modal_button = view.el.querySelector('.show-user-details-modal');
expect(u.isVisible(show_modal_button)).toBeTruthy();
show_modal_button.click();
const modal = view.user_details_modal;
await u.waitUntil(() => u.isVisible(modal.el), 1000);
@ -33,7 +32,7 @@
expect(u.isVisible(remove_contact_button)).toBeTruthy();
remove_contact_button.click();
await u.waitUntil(() => modal.el.getAttribute('aria-hidden'), 1000);
await u.waitUntil(() => !u.isVisible(modal.el));
show_modal_button = view.el.querySelector('.show-user-details-modal');
show_modal_button.click();
remove_contact_button = modal.el.querySelector('button.remove-contact');
@ -51,7 +50,6 @@
await test_utils.openChatBoxFor(_converse, contact_jid)
const view = _converse.chatboxviews.get(contact_jid);
let show_modal_button = view.el.querySelector('.show-user-details-modal');
expect(u.isVisible(show_modal_button)).toBeTruthy();
show_modal_button.click();
const modal = view.user_details_modal;
await u.waitUntil(() => u.isVisible(modal.el), 2000);

View File

@ -0,0 +1,91 @@
import { html } from 'lit-element';
import { CustomElement } from './element.js';
import { until } from 'lit-html/directives/until.js';
import DOMNavigator from "../dom-navigator";
import converse from "@converse/headless/converse-core";
const u = converse.env.utils;
export class Dropdown extends CustomElement {
static get properties () {
return {
'items': { type: Array }
}
}
render () {
return html`
<div class="dropleft">
<button type="button" class="btn btn--transparent btn--standalone" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<i class="fa fa-bars only-icon"></i>
</button>
<div class="dropdown-menu">
${ this.items.map(b => until(b, '')) }
</div>
</div>
`;
}
firstUpdated () {
this.menu = this.querySelector('.dropdown-menu');
this.dropdown = this.firstElementChild;
this.button = this.dropdown.querySelector('button');
this.dropdown.addEventListener('click', ev => this.toggleMenu(ev));
this.dropdown.addEventListener('keyup', ev => this.handleKeyUp(ev));
document.addEventListener('click', ev => !this.contains(ev.target) && this.hideMenu(ev));
this.initArrowNavigation();
}
initArrowNavigation () {
if (!this.navigator) {
const options = {
'selector': '.dropdown-item',
'onSelected': el => el.focus()
};
this.navigator = new DOMNavigator(this.menu, options);
}
}
enableArrowNavigation (ev) {
if (ev) {
ev.preventDefault();
ev.stopPropagation();
}
this.navigator.enable();
this.navigator.select(this.menu.firstElementChild);
}
hideMenu () {
u.removeClass('show', this.menu);
this.navigator.disable();
this.button.setAttribute('aria-expanded', false);
this.button.blur();
}
showMenu () {
u.addClass('show', this.menu);
this.button.setAttribute('aria-expanded', true);
}
toggleMenu () {
if (u.hasClass('show', this.menu)) {
this.hideMenu();
} else {
this.showMenu();
}
}
handleKeyUp (ev) {
if (ev.keyCode === converse.keycodes.ESCAPE) {
this.hideMenu();
} else if (ev.keyCode === converse.keycodes.DOWN_ARROW && !this.navigator.enabled) {
this.enableArrowNavigation(ev);
}
}
}
window.customElements.define('converse-dropdown', Dropdown);

View File

@ -0,0 +1,9 @@
import { LitElement } from 'lit-element';
export class CustomElement extends LitElement {
createRenderRoot () {
// Render without the shadow DOM
return this;
}
}

View File

@ -7,12 +7,10 @@
import "@converse/headless/converse-muc";
import { Model } from 'skeletor.js/src/model.js';
import { View } from 'skeletor.js/src/view.js';
import { html } from "lit-html";
import { __ } from '@converse/headless/i18n';
import converse from "@converse/headless/converse-core";
import tpl_bookmarks_list from "templates/bookmarks_list.js"
import tpl_muc_bookmark_form from "templates/muc_bookmark_form.js";
import tpl_chatroom_bookmark_toggle from "templates/chatroom_bookmark_toggle.html";
const { Strophe, _ } = converse.env;
const u = converse.env.utils;
@ -37,21 +35,24 @@ converse.plugins.add('converse-bookmark-views', {
// plugin architecture they will replace existing methods on the
// relevant objects or classes.
ChatRoomView: {
events: {
'click .toggle-bookmark': 'toggleBookmark'
},
getHeadingButtons () {
const { _converse } = this.__super__;
const buttons = this.__super__.getHeadingButtons.call(this);
if (_converse.allow_bookmarks) {
const supported = _converse.checkBookmarksSupport();
const info_toggle_bookmark = this.model.get('bookmarked') ? __('Unbookmark this groupchat') : __('Bookmark this groupchat');
const bookmarked = this.model.get('bookmarked');
const template = html`<a class="chatbox-btn toggle-bookmark fa fa-bookmark ${bookmarked ? 'button-on' : ''}" title="${info_toggle_bookmark}"></a>`;
const data = {
'i18n_title': bookmarked ? __('Unbookmark this groupchat') : __('Bookmark this groupchat'),
'i18n_text': bookmarked ? __('Unbookmark') : __('Bookmark'),
'handler': ev => this.toggleBookmark(ev),
'a_class': 'toggle-bookmark',
'icon_class': 'fa-bookmark',
'name': 'bookmark'
}
const names = buttons.map(t => t.name);
const idx = names.indexOf('configure');
const template_promise = supported.then(s => s ? template : '');
return idx > -1 ? [...buttons.slice(0, idx), template_promise, ...buttons.slice(idx)] : [template_promise, ...buttons];
const data_promise = supported.then(s => s ? data : '');
return idx > -1 ? [...buttons.slice(0, idx), data_promise, ...buttons.slice(idx)] : [data_promise, ...buttons];
}
return buttons;
}
@ -100,25 +101,6 @@ converse.plugins.add('converse-bookmark-views', {
});
const bookmarkableChatRoomView = {
renderBookmarkToggle () {
const bookmark_button = tpl_chatroom_bookmark_toggle(
_.assignIn(this.model.toJSON(), {
'info_toggle_bookmark': this.model.get('bookmarked') ?
__('Unbookmark this groupchat') :
__('Bookmark this groupchat'),
'bookmarked': this.model.get('bookmarked')
}));
const buttons_row = this.el.querySelector('.chatbox-title__buttons')
const close_button = buttons_row.querySelector('.close-chatbox-button');
if (close_button) {
close_button.insertAdjacentHTML('afterend', bookmark_button);
} else {
buttons_row.insertAdjacentHTML('beforeEnd', bookmark_button);
}
},
/**
* Set whether the groupchat is bookmarked or not.
* @private

View File

@ -7,7 +7,6 @@ import "converse-chatboxviews";
import "converse-message-view";
import "converse-modal";
import { debounce, get, isString } from "lodash";
import { View } from "skeletor.js/src/view";
import { Overview } from "skeletor.js/src/overview";
import { html, render } from "lit-html";
import converse from "@converse/headless/converse-core";
@ -73,78 +72,10 @@ converse.plugins.add('converse-chatview', {
});
_converse.ChatBoxHeading = View.extend({
initialize () {
this.listenTo(this.model, 'change:status', this.onStatusMessageChanged);
this.debouncedRender = debounce(this.render, 50);
this.listenTo(this.model, 'vcard:change', this.debouncedRender);
if (this.model.contact) {
this.listenTo(this.model.contact, 'destroy', this.debouncedRender);
}
if (this.model.rosterContactAdded) {
this.model.rosterContactAdded.then(() => {
this.listenTo(this.model.contact, 'change:nickname', this.debouncedRender);
this.debouncedRender();
});
}
},
render () {
const vcard = get(this.model, 'vcard');
const vcard_json = vcard ? vcard.toJSON() : {};
render(tpl_chatbox_head(
Object.assign(
vcard_json,
this.model.toJSON(),
{ '_converse': _converse,
'buttons': this.getHeadingButtons(),
'display_name': this.model.getDisplayName()
}
)
), this.el);
return this;
},
getHeadingButtons () {
const buttons = [];
if (!_converse.singleton) {
const info_close = __('Close this chat box');
const template = html`<a class="chatbox-btn close-chatbox-button fa fa-times" title="${info_close}"></a>`;
template.name = 'close';
buttons.push(template);
}
const info_details = __('Show more details about this groupchat');
const template = html`<a class="chatbox-btn show-user-details-modal fa fa-id-card" title="${info_details}"></a>`;
template.name = 'details';
buttons.push(template);
return buttons;
},
onStatusMessageChanged (item) {
this.debouncedRender();
/**
* When a contact's custom status message has changed.
* @event _converse#contactStatusMessageChanged
* @type {object}
* @property { object } contact - The chat buddy
* @property { string } message - The message text
* @example _converse.api.listen.on('contactStatusMessageChanged', obj => { ... });
*/
_converse.api.trigger('contactStatusMessageChanged', {
'contact': item.attributes,
'message': item.get('status')
});
}
});
_converse.UserDetailsModal = _converse.BootstrapModal.extend({
id: "user-details-modal",
events: {
'click button.remove-contact': 'removeContact',
'click button.refresh-contact': 'refreshContact',
'click .fingerprint-trust .btn input': 'toggleDeviceTrust'
},
@ -169,11 +100,12 @@ converse.plugins.add('converse-chatview', {
return tpl_user_details_modal(Object.assign(
this.model.toJSON(),
vcard_json, {
'view': this,
'_converse': _converse,
'allow_contact_removal': _converse.allow_contact_removal,
'display_name': this.model.getDisplayName(),
'is_roster_contact': this.model.contact !== undefined,
'removeContact': ev => this.removeContact(ev),
'view': this,
'utils': u
}));
},
@ -208,16 +140,22 @@ converse.plugins.add('converse-chatview', {
const result = confirm(__("Are you sure you want to remove this contact?"));
if (result === true) {
this.modal.hide();
this.model.contact.removeFromRoster(
() => this.model.contact.destroy(),
(err) => {
log.error(err);
_converse.api.alert('error', __('Error'), [
__('Sorry, there was an error while trying to remove %1$s as a contact.',
this.model.contact.getDisplayName())
]);
}
);
// 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);
_converse.api.alert('error', __('Error'), [
__('Sorry, there was an error while trying to remove %1$s as a contact.',
this.model.contact.getDisplayName())
]);
}
);
}, 1);
}
},
});
@ -239,10 +177,8 @@ converse.plugins.add('converse-chatview', {
'click .chat-msg__action-edit': 'onMessageEditButtonClicked',
'click .chat-msg__action-retract': 'onMessageRetractButtonClicked',
'click .chatbox-navback': 'showControlBox',
'click .close-chatbox-button': 'close',
'click .new-msgs-indicator': 'viewUnreadMessages',
'click .send-button': 'onFormSubmitted',
'click .show-user-details-modal': 'showUserDetailsModal',
'click .spoiler-toggle': 'toggleSpoilerMessage',
'click .toggle-call': 'toggleCall',
'click .toggle-clear': 'clearMessages',
@ -258,6 +194,7 @@ converse.plugins.add('converse-chatview', {
async initialize () {
this.initDebounced();
this.listenTo(this.model.messages, 'add', this.onMessageAdded);
this.listenTo(this.model.messages, 'rendered', this.scrollDown);
this.model.messages.on('reset', () => {
@ -265,13 +202,24 @@ converse.plugins.add('converse-chatview', {
this.removeAll();
});
this.listenTo(this.model, 'show', this.show);
this.listenTo(this.model, 'change:status', this.onStatusMessageChanged);
this.listenTo(this.model, 'destroy', this.remove);
this.listenTo(this.model, 'show', this.show);
this.listenTo(this.model, 'vcard:change', this.renderHeading);
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();
await this.updateAfterMessagesFetched();
/**
* Triggered once the {@link _converse.ChatBoxView} has been initialized
* @event _converse#chatBoxViewInitialized
@ -295,7 +243,7 @@ converse.plugins.add('converse-chatview', {
);
this.content = this.el.querySelector('.chat-content');
this.renderMessageForm();
this.insertHeading();
this.renderHeading();
return this;
},
@ -413,13 +361,67 @@ converse.plugins.add('converse-chatview', {
}
},
insertHeading () {
this.heading = new _converse.ChatBoxHeading({'model': this.model});
this.heading.render();
this.heading.chatview = this;
const flyout = this.el.querySelector('.flyout');
flyout.insertBefore(this.heading.el, flyout.querySelector('.chat-body'));
return this;
renderHeading () {
render(this.generateHeadingTemplate(), this.el.querySelector('.chat-head-chatbox'));
},
async 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>`;
},
async 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>`;
},
generateHeadingTemplate () {
const vcard = get(this.model, 'vcard');
const vcard_json = vcard ? vcard.toJSON() : {};
const heading_btns = 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,
'dropdown_btns': dropdown_btns.map(b => this.getHeadingDropdownItem(b)),
'standalone_btns': standalone_btns.map(b => this.getHeadingStandaloneButton(b)),
'display_name': this.model.getDisplayName()
}
)
);
},
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': _converse.view_mode === 'overlayed',
}];
if (!_converse.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': _converse.view_mode === 'overlayed',
});
}
return buttons;
},
getToolbarOptions () {
@ -598,6 +600,22 @@ converse.plugins.add('converse-chatview', {
}
},
onStatusMessageChanged (item) {
this.renderHeading();
/**
* When a contact's custom status message has changed.
* @event _converse#contactStatusMessageChanged
* @type {object}
* @property { object } contact - The chat buddy
* @property { string } message - The message text
* @example _converse.api.listen.on('contactStatusMessageChanged', obj => { ... });
*/
_converse.api.trigger('contactStatusMessageChanged', {
'contact': item.attributes,
'message': item.get('status')
});
},
showHelpMessages (msgs, type='info', spinner) {
msgs.forEach(msg => {
this.content.insertAdjacentHTML(

View File

@ -138,7 +138,8 @@ converse.plugins.add('converse-headlines-view', {
this.listenTo(this.model, 'destroy', this.hide);
this.listenTo(this.model, 'change:minimized', this.onMinimizedChanged);
this.render().insertHeading()
this.render();
this.renderHeading();
this.updateAfterMessagesFetched();
this.insertIntoDOM().hide();
/**

View File

@ -8,7 +8,6 @@ import { Model } from 'skeletor.js/src/model.js';
import { Overview } from "skeletor.js/src/overview";
import { View } from "skeletor.js/src/view";
import { __ } from '@converse/headless/i18n';
import { html } from "lit-html";
import converse from "@converse/headless/converse-core";
import tpl_chats_panel from "templates/chats_panel.html";
import tpl_toggle_chats from "templates/toggle_chats.html";
@ -74,10 +73,6 @@ converse.plugins.add('converse-minimize', {
},
ChatBoxView: {
events: {
'click .toggle-chatbox-button': 'minimize',
},
initialize () {
this.listenTo(this.model, 'change:minimized', this.onMinimizedChanged)
return this.__super__.initialize.apply(this, arguments);
@ -113,25 +108,27 @@ converse.plugins.add('converse-minimize', {
if (!this.model.get('minimized')) {
return this.__super__.setChatBoxWidth.call(this, width);
}
}
},
},
ChatBoxHeading: {
getHeadingButtons () {
const { _converse } = this.__super__;
const buttons = this.__super__.getHeadingButtons.call(this);
const info_minimize = __('Minimize this chat box');
const template = html`<a class="chatbox-btn toggle-chatbox-button fa fa-minus" title="${info_minimize}"></a>`;
const data = {
'a_class': 'toggle-chatbox-button',
'handler': ev => this.minimize(ev),
'i18n_text': __('Minimize'),
'i18n_title': __('Minimize this chat'),
'icon_class': "fa-minus",
'name': 'minimize',
'standalone': _converse.view_mode === 'overlayed'
}
const names = buttons.map(t => t.name);
const idx = names.indexOf('close');
return idx > -1 ? [...buttons.slice(0, idx+1), template, ...buttons.slice(idx+1)] : [template, ...buttons];
return idx > -1 ? [...buttons.slice(0, idx), data, ...buttons.slice(idx)] : [data, ...buttons];
}
},
ChatRoomView: {
events: {
'click .toggle-chatbox-button': 'minimize',
},
initialize () {
this.listenTo(this.model, 'change:minimized', this.onMinimizedChanged)
const result = this.__super__.initialize.apply(this, arguments);
@ -142,12 +139,20 @@ converse.plugins.add('converse-minimize', {
},
getHeadingButtons () {
const { _converse } = this.__super__;
const buttons = this.__super__.getHeadingButtons.call(this);
const info_minimize = __('Minimize this groupchat');
const template = html`<a class="chatbox-btn toggle-chatbox-button fa fa-minus" title="${info_minimize}"></a>`;
const data = {
'a_class': 'toggle-chatbox-button',
'handler': ev => this.minimize(ev),
'i18n_text': __('Minimize'),
'i18n_title': __('Minimize this groupchat'),
'icon_class': "fa-minus",
'name': 'minimize',
'standalone': _converse.view_mode === 'overlayed'
}
const names = buttons.map(t => t.name);
const idx = names.indexOf('signout');
return idx > -1 ? [...buttons.slice(0, idx+1), template, ...buttons.slice(idx+1)] : [template, ...buttons];
return idx > -1 ? [...buttons.slice(0, idx), data, ...buttons.slice(idx)] : [data, ...buttons];
}
}
},

View File

@ -9,7 +9,7 @@ import "@converse/headless/utils/muc";
import { Model } from 'skeletor.js/src/model.js';
import { View } from 'skeletor.js/src/view.js';
import { get, head, isString, isUndefined } from "lodash";
import { html, render } from "lit-html";
import { render } from "lit-html";
import { __ } from '@converse/headless/i18n';
import converse from "@converse/headless/converse-core";
import log from "@converse/headless/log";
@ -649,14 +649,11 @@ converse.plugins.add('converse-muc-views', {
'click .chat-msg__action-edit': 'onMessageEditButtonClicked',
'click .chat-msg__action-retract': 'onMessageRetractButtonClicked',
'click .chatbox-navback': 'showControlBox',
'click .close-chatbox-button': 'close',
'click .configure-chatroom-button': 'getAndRenderConfigurationForm',
'click .hide-occupants': 'hideOccupants',
'click .new-msgs-indicator': 'viewUnreadMessages',
// Arrow functions don't work here because you can't bind a different `this` param to them.
'click .occupant-nick': function (ev) {this.insertIntoTextArea(ev.target.textContent) },
'click .send-button': 'onFormSubmitted',
'click .show-room-details-modal': 'showRoomDetailsModal',
'click .toggle-call': 'toggleCall',
'click .toggle-occupants': 'toggleOccupants',
'click .upload-file': 'toggleFileUpload',
@ -689,6 +686,7 @@ converse.plugins.add('converse-muc-views', {
this.listenTo(this.model, 'show', this.show);
this.listenTo(this.model.features, 'change:moderated', this.renderBottomPanel);
this.listenTo(this.model.features, 'change:open', this.renderHeading);
this.listenTo(this.model.occupants, 'add', this.onOccupantAdded);
this.listenTo(this.model.occupants, 'remove', this.onOccupantRemoved);
@ -738,6 +736,7 @@ converse.plugins.add('converse-muc-views', {
render(this.generateHeadingTemplate(), this.el.querySelector('.chat-head-chatroom'));
},
renderBottomPanel () {
const container = this.el.querySelector('.bottom-panel');
const entered = this.model.session.get('connection_status') === converse.ROOMSTATUS.ENTERED;
@ -1110,40 +1109,100 @@ converse.plugins.add('converse-muc-views', {
},
getHeadingButtons () {
const buttons = [];
if (!_converse.singleton) {
const info_close = __('Close and leave this groupchat');
const template = html`<a class="chatbox-btn close-chatbox-button fa fa-sign-out-alt" title="${info_close}"></a>`;
template.name = 'signout';
buttons.push(template);
const buttons = [{
'i18n_text': __('Details'),
'i18n_title': __('Show more information about this groupchat'),
'handler': ev => this.showRoomDetailsModal(ev),
'a_class': 'show-room-details-modal',
'icon_class': 'fa-info-circle',
'name': 'details'
}];
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'
});
}
if (this.model.getOwnAffiliation() === 'owner') {
const info_configure = __('Configure this groupchat');
const template = html`<a class="chatbox-btn configure-chatroom-button fa fa-wrench" title="${info_configure} "></a>`
template.name = 'configure';
buttons.push(template);
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.get('subject')) {
buttons.push({
'i18n_text': this.model.get('hide_subject') ? __('Show topic') : __('Hide topic'),
'i18n_title': this.model.get('hide_subject') ?
__('Show the topic message in the heading') :
__('Hide the topic in the heading'),
'handler': ev => this.toggleTopic(ev),
'a_class': '',
'icon_class': 'fa-minus-square',
'name': 'toggle-topic'
});
}
if (!_converse.singleton) {
buttons.push({
'i18n_text': __('Leave'),
'i18n_title': __('Leave and close this groupchat'),
'handler': async ev => {
const messages = [__('Are you sure you want to leave this groupchat?')];
const result = await _converse.api.confirm(__('Confirm'), messages);
result && this.close(ev);
},
'a_class': 'close-chatbox-button',
'standalone': _converse.view_mode === 'overlayed',
'icon_class': 'fa-sign-out-alt',
'name': 'signout'
});
}
const info_details = __('Show more details about this groupchat');
const template = html`<a class="chatbox-btn show-room-details-modal fa fa-info-circle" title="${info_details}"></a>`;
template.name = 'details';
buttons.push(template);
return buttons;
},
/**
* Returns the groupchat heading HTML to be rendered.
* Returns the groupchat heading TemplateResult to be rendered.
* @private
* @method _converse.ChatRoomView#generateHeadingTemplate
*/
generateHeadingTemplate () {
const heading_btns = this.getHeadingButtons();
const standalone_btns = heading_btns.filter(b => b.standalone);
const dropdown_btns = heading_btns.filter(b => !b.standalone);
return tpl_chatroom_head(
Object.assign(this.model.toJSON(), {
_converse,
'buttons': this.getHeadingButtons(),
'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.save('hide_subject', !this.model.get('hide_subject'));
},
showInviteModal (ev) {
ev.preventDefault();
if (this.muc_invite_modal === undefined) {
this.muc_invite_modal = new _converse.MUCInviteModal({'model': new Model()});
// TODO: remove once we have API for sending direct invite
this.muc_invite_modal.chatroomview = this;
}
this.muc_invite_modal.show(ev);
},
/**
* Callback method that gets called after the chat has become visible.
* @private
@ -1217,8 +1276,7 @@ converse.plugins.add('converse-muc-views', {
},
/**
* Show or hide the right sidebar containing the chat
* occupants (and the invite widget).
* Hide the right sidebar containing the chat occupants.
* @private
* @method _converse.ChatRoomView#hideOccupants
*/
@ -1232,8 +1290,7 @@ converse.plugins.add('converse-muc-views', {
},
/**
* Show or hide the right sidebar containing the chat
* occupants (and the invite widget).
* Show or hide the right sidebar containing the chat occupants.
* @private
* @method _converse.ChatRoomView#toggleOccupants
*/
@ -1986,26 +2043,13 @@ converse.plugins.add('converse-muc-views', {
// replaced by the user's name.
// Example: Topic set by JC Brand
const message = subject.text ? __('Topic set by %1$s', author) : __('Topic cleared by %1$s', author);
const date = (new Date()).toISOString();
this.content.insertAdjacentHTML(
'beforeend',
tpl_info({
'isodate': date,
'isodate': (new Date()).toISOString(),
'extra_classes': 'chat-event',
'message': message
}));
if (subject.text) {
this.content.insertAdjacentHTML(
'beforeend',
tpl_info({
'isodate': date,
'extra_classes': 'chat-topic',
'message': u.addHyperlinks(xss.filterXSS(get(this.model.get('subject'), 'text'), {'whiteList': {}})),
'render_message': true
}));
}
this.scrollDown();
}
});
@ -2198,9 +2242,7 @@ converse.plugins.add('converse-muc-views', {
Object.assign(this.chatroomview.model.toJSON(), {
_converse,
'features': this.chatroomview.model.features,
'occupants': this.model.models,
'invitesAllowed': () => this.invitesAllowed(),
'showInviteModal': ev => this.showInviteModal(ev)
'occupants': this.model.models
})
);
},
@ -2218,16 +2260,6 @@ converse.plugins.add('converse-muc-views', {
}
},
showInviteModal (ev) {
ev.preventDefault();
if (this.muc_invite_modal === undefined) {
this.muc_invite_modal = new _converse.MUCInviteModal({'model': new Model()});
// TODO: remove once we have API for sending direct invite
this.muc_invite_modal.chatroomview = this.chatroomview;
}
this.muc_invite_modal.show(ev);
},
setOccupantsHeight () {
// TODO: remove the features section in sidebar and then this as well
const el = this.el.querySelector('.chatroom-features');
@ -2235,13 +2267,6 @@ converse.plugins.add('converse-muc-views', {
this.el.querySelector('.occupant-list').style.cssText =
`height: calc(100% - ${el.offsetHeight}px - 5em);`;
}
},
invitesAllowed () {
return _converse.allow_muc_invitations &&
(this.chatroomview.model.features.get('open') ||
this.chatroomview.model.getOwnAffiliation() === "owner"
);
}
});

View File

@ -340,7 +340,6 @@ converse.plugins.add('converse-muc', {
// mention the user and `num_unread_general` to indicate
// generally unread messages (which *includes* mentions!).
'num_unread_general': 0,
'bookmarked': false,
'chat_state': undefined,
'hidden': ['mobile', 'fullscreen'].includes(_converse.view_mode),
@ -348,8 +347,8 @@ converse.plugins.add('converse-muc', {
'name': '',
'num_unread': 0,
'roomconfig': {},
'time_sent': (new Date(0)).toISOString(),
'time_opened': this.get('time_opened') || (new Date()).getTime(),
'time_sent': (new Date(0)).toISOString(),
'type': _converse.CHATROOMS_TYPE
}
},
@ -597,6 +596,13 @@ converse.plugins.add('converse-muc', {
return this;
},
invitesAllowed () {
return _converse.allow_muc_invitations &&
(this.features.get('open') ||
this.getOwnAffiliation() === "owner"
);
},
getDisplayName () {
const name = this.get('name');
if (name) {
@ -1510,7 +1516,10 @@ converse.plugins.add('converse-muc', {
// The subject is changed by sending a message of type "groupchat" to the <room@service>,
// where the <message/> MUST contain a <subject/> element that specifies the new subject but
// MUST NOT contain a <body/> element (or a <thread/> element).
u.safeSave(this, {'subject': {'author': attrs.nick, 'text': attrs.subject || ''}});
u.safeSave(this, {
'subject': {'author': attrs.nick, 'text': attrs.subject || ''},
'hide_subject': attrs.subject ? false : this.get('hide_subject')
});
return true;
}
return false;

View File

@ -131,8 +131,7 @@ u.shouldCreateMessage = function (attrs) {
u.shouldCreateGroupchatMessage = function (attrs) {
return attrs.nick && (u.shouldCreateMessage(attrs) || attrs.is_tombstone);
},
}
u.isEmptyMessage = function (attrs) {
if (attrs instanceof Model) {

View File

@ -1,4 +1,5 @@
<div class="flyout box-flyout">
<div class="chat-head chat-head-chatbox row no-gutters"></div>
<div class="chat-body">
<div class="chat-content {[ if (o.show_send_button) { ]}chat-content-sendbutton{[ } ]}" aria-live="polite"></div>
<div class="bottom-panel">

View File

@ -1,9 +1,7 @@
import { html } from "lit-html";
import { until } from 'lit-html/directives/until.js';
import { __ } from '@converse/headless/i18n';
import { until } from 'lit-html/directives/until.js';
import avatar from "./avatar.js";
import converse from "@converse/headless/converse-core";
import xss from "xss/dist/xss";
const i18n_profile = __('The User\'s Profile Image');
@ -14,20 +12,24 @@ const avatar_data = {
'width': 40,
}
const tpl_standalone_btns = (o) => o.standalone_btns.reverse().map(b => until(b, ''));
export default (o) => {
return html`
<div class="chat-head chat-head-chatbox row no-gutters">
<div class="chatbox-title">
<div class="chatbox-title--row">
${ (!o._converse.singleton) ? html`<div class="chatbox-navback"><i class="fa fa-arrow-left"></i></div>` : '' }
${ (o.type !== o._converse.HEADLINES_TYPE) ? avatar(Object.assign({}, o, avatar_data)) : '' }
<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}
</div>
<div class="chatbox-title ${ o.status ? '' : "chatbox-title--no-desc"}">
<div class="chatbox-title--row">
${ (!o._converse.singleton) ? html`<div class="chatbox-navback"><i class="fa fa-arrow-left"></i></div>` : '' }
${ (o.type !== o._converse.HEADLINES_TYPE) ? avatar(Object.assign({}, o, avatar_data)) : '' }
<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}
</div>
<div class="chatbox-title__buttons row no-gutters">${ o.buttons.map(b => until(b, '')) }</div>
</div>
<p class="chat-head__desc">${ o.status }</p>
<div class="chatbox-title__buttons row no-gutters">
${ o.dropdown_btns.length ? html`<converse-dropdown .items=${o.dropdown_btns}></converse-dropdown>` : '' }
${ o.standalone_btns.length ? tpl_standalone_btns(o) : '' }
</div>
</div>
${ o.status ? html`<p class="chat-head__desc">${ o.status }</p>` : '' }
`;
}

View File

@ -1,3 +1,5 @@
import '../components/dropdown.js';
import { __ } from '@converse/headless/i18n';
import { html } from "lit-html";
import { unsafeHTML } from 'lit-html/directives/unsafe-html.js';
import { until } from 'lit-html/directives/until.js';
@ -5,18 +7,26 @@ import converse from "@converse/headless/converse-core";
import xss from "xss/dist/xss";
const u = converse.env.utils;
const i18n_hide_topic = __('Hide the groupchat topic');
const tpl_standalone_btns = (o) => o.standalone_btns.reverse().map(b => until(b, ''));
export default (o) => {
const subject = o.subject ? u.addHyperlinks(xss.filterXSS(o.subject.text, {'whiteList': {}})) : '';
const show_subject = (subject && !o.hide_subject);
return html`
<div class="chatbox-title">
${ (!o._converse.singleton) ? html`<div class="chatbox-navback"><i class="fa fa-arrow-left"></i></div>` : '' }
<div class="chatbox-title__text" title="${ (o._converse.locked_muc_domain !== 'hidden') ? o.jid : '' }">${ o.title }</div>
<div class="chatbox-title ${ show_subject ? '' : "chatbox-title--no-desc"}">
${ (o._converse.standalone) ? html`<div class="chatbox-navback"><i class="fa fa-arrow-left"></i></div>` : '' }
<div class="chatbox-title__text" title="${ (o._converse.locked_muc_domain !== 'hidden') ? o.jid : '' }">${ o.title }
${ (o.bookmarked) ? html`<i class="fa fa-bookmark"></i>` : '' }
</div>
<div class="chatbox-title__buttons row no-gutters">
${ o.buttons.map(b => until(b, '')) }
${ o.standalone_btns.length ? tpl_standalone_btns(o) : '' }
${ o.dropdown_btns.length ? html`<converse-dropdown .items=${o.dropdown_btns}></converse-dropdown>` : '' }
</div>
</div>
<p class="chat-head__desc">${unsafeHTML(subject)}</p>
${ show_subject ? html`<p class="chat-head__desc" title="${i18n_hide_topic}">${unsafeHTML(subject)}</p>` : '' }
`;
}

View File

@ -13,30 +13,10 @@ const PRETTY_CHAT_STATUS = {
'online': 'Online'
};
const occupant_hint = (occupant) => __('Click to mention %1$s in your message.', occupant.get('nick'))
const i18n_invite = (o) => o._converse.view_mode === 'overlayed' ? __('Invite') : __('Invite someone');
const i18n_invite_hint = __('Invite someone to join this groupchat');
const i18n_occupant_hint = (occupant) => __('Click to mention %1$s in your message.', occupant.get('nick'))
const i18n_participants = __('Participants');
const invite_widget = (o) => {
if (o.invitesAllowed()) {
return html`
<a class="open-invite-modal"
title="${i18n_invite_hint}"
data-toggle="modal"
data-target="#muc-invite-modal"
@click=${o.showInviteModal}>
<i class="btn btn-primary btn-circle fa fa-user-plus"></i>
${i18n_invite(o)}
</a>`;
} else {
return '';
}
}
export default (o) => html`
<div class="occupants-header">
<i class="hide-occupants fa fa-times"></i>
@ -51,10 +31,9 @@ export default (o) => html`
Object.assign({
'jid': '',
'hint_show': PRETTY_CHAT_STATUS[occupant.get('show')],
'hint_occupant': occupant_hint(occupant)
'hint_occupant': i18n_occupant_hint(occupant)
}, occupant.toJSON())
);
}) }
</ul>
${ invite_widget(o) }
`;

View File

@ -60,7 +60,13 @@ const fingerprints = (o) => {
`;
}
const remove_button = html`<button type="button" class="btn btn-danger remove-contact"><i class="far fa-trash-alt"> </i>${i18n_remove_contact}</button>`;
const remove_button = (o) => {
return html`
<button type="button" @click="${o.removeContact}" class="btn btn-danger remove-contact">
<i class="far fa-trash-alt"></i>${i18n_remove_contact}
</button>
`;
}
export default (o) => html`
@ -84,7 +90,7 @@ export default (o) => html`
<div class="modal-footer">
${modal_close_button}
<button type="button" class="btn btn-info refresh-contact"><i class="fa fa-refresh"> </i>${i18n_refresh}</button>
${ (o.allow_contact_removal && o.is_roster_contact) ? remove_button : '' }
${ (o.allow_contact_removal && o.is_roster_contact) ? remove_button(o) : '' }
</div>
</div>

View File

@ -275,6 +275,11 @@ u.hasClass = function (className, el) {
return (el instanceof Element) && el.classList.contains(className);
};
u.toggleClass = function (className, el) {
u.hasClass(className, el) ? u.removeClass(className, el) : u.addClass(className, el);
}
/**
* Add a class to an element.
* @method u#addClass