Move MUC invite widget into a modal

This commit is contained in:
JC Brand 2020-01-27 13:20:23 +01:00
parent c6ac03e94e
commit 9fb2056753
8 changed files with 186 additions and 143 deletions

4
package-lock.json generated
View File

@ -16094,8 +16094,8 @@
"dev": true
},
"skeletor.js": {
"version": "github:skeletorjs/skeletor#29a6d8f707076e865133b8f36f07c76ba4b4b582",
"from": "github:skeletorjs/skeletor#29a6d8f707076e865133b8f36f07c76ba4b4b582",
"version": "github:skeletorjs/skeletor#9a4487496bd2810b2f0847acbca136333cf9cfb0",
"from": "github:skeletorjs/skeletor#9a4487496bd2810b2f0847acbca136333cf9cfb0",
"requires": {
"lodash": "^4.17.14"
}

View File

@ -172,12 +172,25 @@
.hide-occupants {
align-self: flex-end;
cursor: pointer;
font-size: var(--font-size-small);
}
}
.occupants-header--title {
margin-top: 0.5em;
margin-bottom: 0.5em;
display: flex;
flex-direction: row;
.fa-user-plus {
margin-top: 0.2em;
}
}
.occupants-heading {
font-family: var(--heading-font);
margin-bottom: 0.5em;
color: var(--chatroom-head-color);
padding-left: 0;
margin-right: 1em;
}
.chatroom-features {
display: var(--occupants-features-display);

View File

@ -1940,36 +1940,54 @@
'muc_anonymous'
]
await test_utils.openAndEnterChatRoom(_converse, 'lounge@montague.lit', 'romeo', features);
spyOn(_converse.api, "trigger").and.callThrough();
spyOn(window, 'prompt').and.callFake(() => "Please join!");
const view = _converse.chatboxviews.get('lounge@montague.lit');
expect(view.model.getOwnAffiliation()).toBe('owner');
expect(view.model.features.get('open')).toBe(false);
expect(view.el.querySelectorAll('input.invited-contact').length).toBe(1);
expect(view.el.querySelector('.occupants-header .fa-user-plus')).not.toBe(null);
// Members can't invite if the room isn't open
view.model.getOwnOccupant().set('affiliation', 'member');
await u.waitUntil(() => view.el.querySelectorAll('input.invited-contact').length === 0);
await u.waitUntil(() => view.el.querySelector('.occupants-header .fa-user-plus') === null);
view.model.features.set('open', 'true');
await u.waitUntil(() => view.el.querySelector('.occupants-header .fa-user-plus'));
view.el.querySelector('.occupants-header .fa-user-plus').click();
const modal = view.sidebar_view.muc_invite_modal;
await u.waitUntil(() => u.isVisible(modal.el), 1000)
expect(modal.el.querySelectorAll('#invitee_jids').length).toBe(1);
expect(modal.el.querySelectorAll('textarea').length).toBe(1);
spyOn(view.model, 'directInvite').and.callThrough();
await u.waitUntil(() => view.el.querySelectorAll('input.invited-contact').length);
const input = view.el.querySelector('input.invited-contact');
expect(input.getAttribute('placeholder')).toBe('Invite');
const input = modal.el.querySelector('#invitee_jids');
input.value = "Balt";
modal.el.querySelector('button[type="submit"]').click();
await u.waitUntil(() => modal.el.querySelector('.error'));
const error = modal.el.querySelector('.error');
expect(error.textContent).toBe('Please enter a valid XMPP address');
let evt = new Event('input');
input.dispatchEvent(evt);
let sent_stanza;
spyOn(_converse.connection, 'send').and.callFake(stanza => (sent_stanza = stanza));
const hint = await u.waitUntil(() => view.el.querySelector('.suggestion-box__results li'));
const hint = await u.waitUntil(() => modal.el.querySelector('.suggestion-box__results li'));
expect(input.value).toBe('Balt');
expect(hint.textContent.trim()).toBe('Balthasar');
evt = new Event('mousedown', {'bubbles': true});
evt.button = 0;
hint.dispatchEvent(evt);
expect(window.prompt).toHaveBeenCalled();
const textarea = modal.el.querySelector('textarea');
textarea.value = "Please join!";
modal.el.querySelector('button[type="submit"]').click();
expect(view.model.directInvite).toHaveBeenCalled();
expect(sent_stanza.toLocaleString()).toBe(
`<message from="romeo@montague.lit/orchard" `+

View File

@ -23,7 +23,7 @@ import tpl_chatroom_details_modal from "templates/chatroom_details_modal.js";
import tpl_chatroom_disconnect from "templates/chatroom_disconnect.html";
import tpl_muc_config_form from "templates/muc_config_form.js";
import tpl_chatroom_head from "templates/chatroom_head.html";
import tpl_chatroom_invite from "templates/chatroom_invite.html";
import tpl_muc_invite_modal from "templates/muc_invite_modal.js";
import tpl_chatroom_nickname_form from "templates/chatroom_nickname_form.html";
import tpl_muc_password_form from "templates/muc_password_form.js";
import tpl_muc_sidebar from "templates/muc_sidebar.js";
@ -701,7 +701,7 @@ converse.plugins.add('converse-muc-views', {
this.onMouseUp = this.onMouseUp.bind(this);
this.render();
this.createOccupantsView();
this.createSidebarView();
await this.updateAfterMessagesFetched();
this.onConnectionStatusChanged();
/**
@ -770,15 +770,15 @@ converse.plugins.add('converse-muc-views', {
return this;
},
createOccupantsView () {
createSidebarView () {
this.model.occupants.chatroomview = this;
this.occupants_view = new _converse.ChatRoomOccupantsView({'model': this.model.occupants});
this.sidebar_view = new _converse.MUCSidebar({'model': this.model.occupants});
const container_el = this.el.querySelector('.chatroom-body');
const occupants_width = this.model.get('occupants_width');
if (this.occupants_view && occupants_width !== undefined) {
this.occupants_view.el.style.flex = "0 0 " + occupants_width + "px";
if (this.sidebar_view && occupants_width !== undefined) {
this.sidebar_view.el.style.flex = "0 0 " + occupants_width + "px";
}
container_el.insertAdjacentElement('beforeend', this.occupants_view.el);
container_el.insertAdjacentElement('beforeend', this.sidebar_view.el);
},
onStartResizeOccupants (ev) {
@ -786,7 +786,7 @@ converse.plugins.add('converse-muc-views', {
this.el.addEventListener('mousemove', this.onMouseMove);
this.el.addEventListener('mouseup', this.onMouseUp);
const style = window.getComputedStyle(this.occupants_view.el);
const style = window.getComputedStyle(this.sidebar_view.el);
this.width = parseInt(style.width.replace(/px$/, ''), 10);
this.prev_pageX = ev.pageX;
},
@ -795,7 +795,7 @@ converse.plugins.add('converse-muc-views', {
if (this.resizing) {
ev.preventDefault();
const delta = this.prev_pageX - ev.pageX;
this.resizeOccupantsView(delta, ev.pageX);
this.resizeSidebarView(delta, ev.pageX);
this.prev_pageX = ev.pageX;
}
},
@ -806,26 +806,26 @@ converse.plugins.add('converse-muc-views', {
this.resizing = false;
this.el.removeEventListener('mousemove', this.onMouseMove);
this.el.removeEventListener('mouseup', this.onMouseUp);
const element_position = this.occupants_view.el.getBoundingClientRect();
const occupants_width = this.calculateOccupantsWidth(element_position, 0);
const element_position = this.sidebar_view.el.getBoundingClientRect();
const occupants_width = this.calculateSidebarWidth(element_position, 0);
const attrs = {occupants_width};
_converse.connection.connected ? this.model.save(attrs) : this.model.set(attrs);
}
},
resizeOccupantsView (delta, current_mouse_position) {
const element_position = this.occupants_view.el.getBoundingClientRect();
resizeSidebarView (delta, current_mouse_position) {
const element_position = this.sidebar_view.el.getBoundingClientRect();
if (this.is_minimum) {
this.is_minimum = element_position.left < current_mouse_position;
} else if (this.is_maximum) {
this.is_maximum = element_position.left > current_mouse_position;
} else {
const occupants_width = this.calculateOccupantsWidth(element_position, delta);
this.occupants_view.el.style.flex = "0 0 " + occupants_width + "px";
const occupants_width = this.calculateSidebarWidth(element_position, delta);
this.sidebar_view.el.style.flex = "0 0 " + occupants_width + "px";
}
},
calculateOccupantsWidth(element_position, delta) {
calculateSidebarWidth(element_position, delta) {
let occupants_width = element_position.width + delta;
const room_width = this.el.clientWidth;
// keeping display in boundaries
@ -2120,19 +2120,62 @@ converse.plugins.add('converse-muc-views', {
});
_converse.ChatRoomOccupantsView = HTMLView.extend({
_converse.MUCInviteModal = _converse.BootstrapModal.extend({
id: "muc-invite-modal",
initialize () {
_converse.BootstrapModal.prototype.initialize.apply(this, arguments);
this.listenTo(this.model, 'change', this.render);
this.initInviteWidget();
},
toHTML () {
return tpl_muc_invite_modal(Object.assign(
this.model.toJSON(), {
'submitInviteForm': ev => this.submitInviteForm(ev)
})
);
},
initInviteWidget () {
if (this.invite_auto_complete) {
this.invite_auto_complete.destroy();
}
const list = _converse.roster.map(i => ({'label': i.getDisplayName(), 'value': i.get('jid')}));
const el = this.el.querySelector('.suggestion-box').parentElement;
this.invite_auto_complete = new _converse.AutoComplete(el, {
'min_chars': 1,
'list': list
});
},
submitInviteForm (ev) {
ev.preventDefault();
// TODO: Add support for sending an invite to multiple JIDs
const data = new FormData(ev.target);
const jid = data.get('invitee_jids');
const reason = data.get('reason');
if (u.isValidJID(jid)) {
// TODO: Create and use API here
this.chatroomview.model.directInvite(jid, reason);
this.modal.hide();
} else {
this.model.set({'invalid_invite_jid': true});
}
}
});
_converse.MUCSidebar = HTMLView.extend({
tagName: 'div',
className: 'occupants col-md-3 col-4',
async initialize () {
this.chatroomview = this.model.chatroomview;
this.listenTo(this.model, 'add', this.maybeRenderInviteWidget);
this.listenTo(this.model, 'add', this.render);
this.listenTo(this.model, 'remove', this.render);
this.listenTo(this.model, 'change', this.render);
this.listenTo(this.model, 'change:affiliation', this.maybeRenderInviteWidget);
this.listenTo(this.chatroomview.model.features, 'change', this.render);
this.listenTo(this.chatroomview.model.features, 'change:open', this.renderInviteWidget);
this.listenTo(this.chatroomview.model, 'change:hidden_occupants', this.setVisibility);
this.render();
await this.model.fetched;
@ -2141,21 +2184,16 @@ converse.plugins.add('converse-muc-views', {
toHTML () {
return tpl_muc_sidebar(
Object.assign(this.chatroomview.model.toJSON(), {
'allow_muc_invitations': _converse.allow_muc_invitations,
'features': this.chatroomview.model.features,
'label_occupants': __('Participants'),
'occupants': this.model.models
'occupants': this.model.models,
'invitesAllowed': () => this.invitesAllowed(),
'showInviteModal': ev => this.showInviteModal(ev)
})
);
},
afterRender () {
if (_converse.allow_muc_invitations) {
// TODO: the invite widget needs to be rendered via a directive
_converse.api.waitUntil('rosterContactsFetched').then(() => this.renderInviteWidget());
}
this.setVisibility();
this.setOccupantsHeight();
},
setVisibility () {
@ -2167,34 +2205,18 @@ converse.plugins.add('converse-muc-views', {
}
},
maybeRenderInviteWidget (occupant) {
if (occupant.get('jid') === _converse.bare_jid) {
this.renderInviteWidget();
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;
}
},
renderInviteWidget () {
// TODO: this needs to be rendered inside muc_sidebar.js
const widget = this.el.querySelector('.room-invite');
if (this.shouldInviteWidgetBeShown()) {
if (widget === null) {
const heading = this.el.querySelector('.occupants-heading');
heading.insertAdjacentHTML(
'afterend',
tpl_chatroom_invite({
'error_message': null,
'label_invitation': __('Invite'),
})
);
this.initInviteWidget();
}
} else if (widget !== null) {
widget.remove();
}
return this;
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');
if (el) {
this.el.querySelector('.occupant-list').style.cssText =
@ -2202,74 +2224,12 @@ converse.plugins.add('converse-muc-views', {
}
},
promptForInvite (suggestion) {
let reason = '';
if (!_converse.auto_join_on_invite) {
reason = prompt(
__('You are about to invite %1$s to the groupchat "%2$s". '+
'You may optionally include a message, explaining the reason for the invitation.',
suggestion.text.label, this.chatroomview.model.getDisplayName())
);
}
if (reason !== null) {
this.chatroomview.model.directInvite(suggestion.text.value, reason);
}
const form = this.el.querySelector('.room-invite form'),
input = form.querySelector('.invited-contact'),
error = form.querySelector('.error');
if (error !== null) {
error.parentNode.removeChild(error);
}
input.value = '';
},
inviteFormSubmitted (evt) {
evt.preventDefault();
const el = evt.target.querySelector('input.invited-contact');
const jid = el.value;
if (u.isValid(jid)) {
this.promptForInvite({
'target': el,
'text': {'label': jid, 'value': jid}}
);
} else {
evt.target.outerHTML = tpl_chatroom_invite({
'error_message': __('Please enter a valid XMPP address'),
'label_invitation': __('Invite'),
});
this.initInviteWidget();
}
},
shouldInviteWidgetBeShown () {
invitesAllowed () {
return _converse.allow_muc_invitations &&
(this.chatroomview.model.features.get('open') ||
this.chatroomview.model.getOwnAffiliation() === "owner"
);
},
initInviteWidget () {
const form = this.el.querySelector('.room-invite form');
if (form === null) {
return;
}
form.addEventListener('submit', this.inviteFormSubmitted.bind(this), false);
const list = _converse.roster.map(i => ({'label': i.getDisplayName(), 'value': i.get('jid')}));
const el = this.el.querySelector('.suggestion-box').parentElement;
if (this.invite_auto_complete) {
this.invite_auto_complete.destroy();
}
this.invite_auto_complete = new _converse.AutoComplete(el, {
'min_chars': 1,
'list': list
});
this.invite_auto_complete.on('suggestion-box-selectcomplete', ev => this.promptForInvite(ev));
this.invite_auto_complete.on('suggestion-box-open', () => {
this.invite_auto_complete.ul.setAttribute('style', `max-height: calc(${this.el.offsetHeight}px - 80px);`);
});
},
}
});

View File

@ -26,7 +26,7 @@
},
"gitHead": "9641dcdc820e029b05930479c242d2b707bbe8e2",
"devDependencies": {
"skeletor.js": "skeletorjs/skeletor#29a6d8f707076e865133b8f36f07c76ba4b4b582",
"skeletor.js": "skeletorjs/skeletor#9a4487496bd2810b2f0847acbca136333cf9cfb0",
"backbone": "1.4",
"backbone.browserStorage": "conversejs/backbone.browserStorage#674ba3aa0e4d0f0b0dcac48fcc7dea531012828f",
"filesize": "^4.1.2",

View File

@ -1,12 +0,0 @@
<div class="suggestion-box room-invite">
<form>
{[ if (o.error_message) { ]} <div class="error error-feedback">{{{o.error_message}}}</div> {[ } ]}
<div class="form-group">
<input class="form-control invited-contact suggestion-box__input"
placeholder="{{{o.label_invitation}}}"
type="text"/>
<span class="suggestion-box__additions visually-hidden" role="status" aria-live="assertive" aria-relevant="additions"></span>
</div>
</form>
<ul class="suggestion-box__results suggestion-box__results--below" hidden=""></ul>
</div>

View File

@ -0,0 +1,46 @@
import { html } from "lit-html";
import { __ } from '@converse/headless/i18n';
import { modal_header_close_button } from "./buttons"
const i18n_invite = __('Invite');
const i18n_invite_heading = __('Invite someone to this groupchat');
const error_message = __('Please enter a valid XMPP address');
const i18n_invite_label = __('XMPP Address');
const i18n_reason = __('Optional reason for the invitation');
export default (o) => html`
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="add-chatroom-modal-label">${i18n_invite_heading}</h5>
${modal_header_close_button}
</div>
<div class="modal-body">
<span class="modal-alert"></span>
<div class="suggestion-box room-invite">
<form @submit=${o.submitInviteForm}>
<div class="form-group">
<label class="clearfix" for="invitee_jids">${i18n_invite_label}:</label>
${ o.invalid_invite_jid ? html`<div class="error error-feedback">${error_message}</div>` : '' }
<input class="form-control suggestion-box__input"
required="required"
name="invitee_jids"
id="invitee_jids"
type="text"/>
<span class="suggestion-box__additions visually-hidden" role="status" aria-live="assertive" aria-relevant="additions"></span>
<ul class="suggestion-box__results suggestion-box__results--below" hidden=""></ul>
</div>
<div class="form-group">
<label>${i18n_reason}:</label>
<textarea class="form-control" name="reason"></textarea>
</div>
<div class="form-group">
<button type="submit" class="btn btn-primary">${i18n_invite}</button>
</div>
</form>
</div>
</div>
</div>
</div>
`;

View File

@ -20,6 +20,7 @@ const i18n_archived = __('Message archiving');
const i18n_archived_hint = __('Messages are archived on the server');
const i18n_features = __('Features');
const i18n_hidden = __('Hidden');
const i18n_invite_hint = __('Invite people to join this groupchat');
const i18n_members_only = __('Members only');
const i18n_members_only_hint = __('this groupchat is restricted to members only');
const i18n_moderated = __('Moderated');
@ -32,6 +33,7 @@ const i18n_not_moderated = __('Not moderated');
const i18n_not_searchable_hint = __('This groupchat is not publicly searchable');
const i18n_open = __('Open');
const i18n_open_hint = __('Anyone can join this groupchat');
const i18n_participants = __('Participants');
const i18n_password = __('Password protected')
const i18n_password_hint = __('This groupchat requires a password before entry');
const i18n_persistent = __('Persistent');
@ -77,11 +79,27 @@ const tpl_features = (o) => html`
</div>
`;
const invite_button = (o) => {
if (o.invitesAllowed()) {
return html`
<a class="fa fa-user-plus"
title="${i18n_invite_hint}"
@click=${o.showInviteModal}
data-toggle="modal"
data-target="#muc-invite-modal"></a>`;
} else {
return '';
}
}
export default (o) => html`
<div class="occupants-header">
<i class="hide-occupants fa fa-times"></i>
<p class="occupants-heading">${o.label_occupants}</p>
<div class="occupants-header--title">
<span class="occupants-heading">${i18n_participants}</span>
${ invite_button(o) }
</div>
</div>
<div class="dragresize dragresize-occupants-left"></div>
<ul class="occupant-list">