Commit f387c947 authored by JC Brand's avatar JC Brand

Allow the full app to be embedded.

- new config option `singleton`.
- new plugin `converse-uniview`
- removed `converse-embedded`.
- various CSS changes, to properly render an embedded full app
- don't re-open cached and non-autojoined chats in singleton mode

The goal here is to extend the `embedded` `view_mode` so that the full app can
also be embedded, not just a single MUC or private chat.

To do this, we'll need to differentiate between multi and singleton chat apps.

* A singleton chat app contains only a single chat.
* A multi-chat app can contain zero or more chats

So we introduce a new config option, `singleton`, which when used with
`view_mode` set to `embedded` will determine whether a single chat or the full
app is embedded.

Similarly, in `overlayed`, `fullscreen` and `mobile` view modes, `singleton`
set to true will allow only one chat within the parameters of that view mode.

We're appropriating the word `singleton` and introducing the concepts of
`uniview` and `multiview` (see a785ca8) to cover what was
previously meant with `singleton`.

updates #1297
parent 2d507c68
......@@ -21,6 +21,9 @@ pip-selfcheck.json
3rdparty/libsignal-protocol-javascript/
*.map
locale.zip
sounds.zip
analytics.js
# virtualenv/python/buildout
......
......@@ -13,6 +13,10 @@
- Replace `moment` with [DayJS](https://github.com/iamkun/dayjs).
- New API method [\_converse.api.disco.features.get](https://conversejs.org/docs/html/api/-_converse.api.disco.features.html#.get)
- New config setting [muc_show_join_leave_status](https://conversejs.org/docs/html/configuration.html#muc-show-join-leave-status)
- New config option [singleton](https://conversejs.org/docs/html/configuration.html#singleton).
By setting this option to `false` and `view_mode` to `'embedded'`, it's now possible to
"embed" the full app and not just a single chat. To embed just a single chat,
it's now necessary to explicitly set `singleton` to `true`.
- New event: `chatBoxBlurred`.
- New event: [chatBoxBlurred](https://conversejs.org/docs/html/api/-_converse.html#event:chatBoxBlurred)
- New event: [chatReconnected](https://conversejs.org/docs/html/api/-_converse.html#event:chatReconnected)
......
......@@ -90,6 +90,7 @@
notify_all_room_messages: [
'anonymous@conference.nomnom.im',
],
singleton: true,
locales_url: "../locale/{{{locale}}}/LC_MESSAGES/converse.json",
view_mode: 'embedded',
});
......
......@@ -1383,6 +1383,21 @@ show_send_button
If set to ``true``, a button will be visible which can be clicked to send a message.
singleton
---------
* Default: ``false``
If set to ``true``, then only one chat (one-on-one or groupchat) will be allowed.
The chat must be specified with the `auto_join_rooms`_ or `auto_join_private_chats`_ options.
This setting is useful together with `view_mode`_ set to ``embedded``, when you
want to embed a chat into the page.
Alternatively you could use it with `view_mode`_ set to ``overlayed`` to create
a single helpdesk-type chat.
sounds_path
-----------
......
......@@ -474,12 +474,6 @@
/* ******************* Overlay and embedded styles *************************** */
#conversejs.converse-embedded {
.chat-textarea {
max-height: var(--fullpage-max-chat-textarea-height);
}
}
#conversejs.converse-embedded,
#conversejs.converse-overlayed {
.chat-head {
......@@ -580,8 +574,31 @@
}
}
/* ******************* Fullpage styles *************************** */
#conversejs.converse-embedded.converse-singleton {
.flyout {
border: none !important;
}
.chat-head {
height: var(--fullpage-chat-head-height);
padding: 0.5em;
}
.chatbox {
margin: 0;
@include make-col-ready();
@include media-breakpoint-up(md) {
@include make-col(12);
}
@include media-breakpoint-up(lg) {
@include make-col(12);
}
@include media-breakpoint-up(xl) {
@include make-col(12);
}
}
}
#conversejs.converse-embedded,
#conversejs.converse-fullscreen {
.flyout {
border-radius: 0;
......@@ -589,13 +606,9 @@
border: var(--flyout-padding) solid var(--chat-head-color);
bottom: 0;
}
.chatbox-btn {
font-size: var(--fullpage-chatbox-button-size);
margin: 0 0.3em;
}
.chat-head {
height: var(--fullpage-chat-head-height);
font-size: var(--font-size-huge);
padding: 0;
.user-custom-message {
font-size: 70%;
......@@ -609,18 +622,9 @@
@include make-col(2);
}
}
.chat-textarea {
max-height: var(--fullpage-max-chat-textarea-height);
}
.emoji-picker {
height: var(--fullpage-emoji-picker-height);
}
.chatbox {
width: 100%;
height: 100%;
margin: 0;
@include make-col-ready();
@include media-breakpoint-up(md) {
@include make-col(8);
......@@ -632,6 +636,60 @@
@include make-col(10);
}
.box-flyout {
box-shadow: none;
overflow: hidden;
}
}
}
#conversejs.converse-embedded {
.converse-chatboxes {
z-index: 1031; // One more than bootstrap navbar
position: inherit;
flex-wrap: nowrap;
bottom: auto;
height: 100%;
width: 100%;
margin-left: -15px;
}
.chatbox {
.box-flyout {
bottom: 0;
height: 100%;
min-width: auto;
width: 100%;
}
.chat-title {
padding: 0.3em;
font-size: 120%;
}
}
.chat-textarea {
max-height: var(--fullpage-max-chat-textarea-height);
}
}
/* ******************* Fullpage styles *************************** */
#conversejs.converse-fullscreen {
.chatbox-btn {
font-size: var(--fullpage-chatbox-button-size);
margin: 0 0.3em;
}
.chat-head {
font-size: var(--font-size-huge);
}
.chat-textarea {
max-height: var(--fullpage-max-chat-textarea-height);
}
.emoji-picker {
height: var(--fullpage-emoji-picker-height);
}
.chatbox {
.box-flyout {
background-color: var(--chat-head-color);
box-shadow: none;
......
......@@ -429,6 +429,7 @@
}
}
#conversejs.converse-fullscreen {
.chatroom {
.box-flyout {
......@@ -462,6 +463,7 @@
}
}
#conversejs.converse-embedded,
#conversejs.converse-fullscreen,
#conversejs.converse-mobile {
......@@ -519,3 +521,42 @@
}
}
}
#conversejs.converse-embedded {
.chatroom {
margin: 0;
width: 100%;
.box-flyout {
.occupants-heading {
font-size: 120%;
}
.chat-content {
.chat-message {
margin: 0.5em;
font-size: 120%;
}
}
.sendXMPPMessage {
.chat-textarea {
padding: 0.5em;
font-size: 110%;
}
}
.chatroom-body {
height: 100%;
.chatroom-form-container {
height: 100%;
position: relative;
}
}
.occupants {
.occupant-list {
padding-left: 0.3em;
li.occupant {
font-size: 120%;
}
}
}
}
}
}
......@@ -435,6 +435,7 @@
}
}
#conversejs.converse-embedded,
#conversejs.converse-fullscreen,
#conversejs.converse-mobile {
#controlbox {
......@@ -545,7 +546,7 @@
@include media-breakpoint-down(sm) {
#conversejs:not(.converse-embedded) {
#conversejs {
left: 0;
right: 0;
padding-left: env(safe-area-inset-left);
......
......@@ -120,6 +120,17 @@ body.converse-fullscreen {
&.converse-overlayed {
height: 3em;
}
&.converse-embedded {
@include box-sizing(border-box);
*, *:before, *:after {
@include box-sizing(border-box);
}
bottom: auto;
height: 100%; // When embedded, it fills the containing element
position: relative;
right: auto;
width: 100%;
}
.brand-heading-container {
text-align: center;
......
#conversejs.converse-embedded {
@include box-sizing(border-box);
*, *:before, *:after {
@include box-sizing(border-box);
}
bottom: auto;
height: 100%; // When embedded, it fills the containing element
position: relative;
right: auto;
width: 100%;
.converse-chatboxes {
z-index: 1031; // One more than bootstrap navbar
position: inherit;
bottom: auto;
height: 100%;
width: 100%;
}
.chatbox {
margin: 0;
height: 100%;
width: 100%;
.flyout.box-flyout {
bottom: 0;
box-shadow: none;
height: 100%;
min-width: auto;
width: 100%;
}
.chat-title {
padding: 0.3em;
font-size: 120%;
}
}
.chatbox-btn {
display: none;
}
.chatroom {
margin: 0;
width: 100%;
.box-flyout {
.occupants-heading {
font-size: 120%;
}
.chat-content {
.chat-message {
margin: 0.5em;
font-size: 120%;
}
}
.sendXMPPMessage {
.chat-textarea {
padding: 0.5em;
font-size: 110%;
}
}
.chatroom-body {
height: 100%;
.chatroom-form-container {
height: 100%;
position: relative;
}
}
.occupants {
.occupant-list {
padding-left: 0.3em;
li.occupant {
font-size: 120%;
}
}
}
}
}
}
......@@ -63,4 +63,3 @@
@import "minimized_chats";
@import "bookmarks";
@import "autocomplete";
@import "embedded";
......@@ -115,15 +115,11 @@
it("is highlighted if its currently open", mock.initConverse(
null, ['rosterGroupsFetched', 'chatBoxesFetched'],
{ whitelisted_plugins: ['converse-roomslist'],
allow_bookmarks: false // Makes testing easier, otherwise we
// have to mock stanza traffic.
{ view_mode: 'fullscreen',
allow_bookmarks: false // Makes testing easier, otherwise we have to mock stanza traffic.
}, async function (done, _converse) {
spyOn(_converse, 'isUniView').and.callFake(() => true);
let room_els, item;
test_utils.openControlBox();
await _converse.api.rooms.open('coven@chat.shakespeare.lit', {'nick': 'some1'});
room_els = _converse.rooms_list_view.el.querySelectorAll(".available-chatroom");
expect(room_els.length).toBe(1);
......@@ -139,6 +135,8 @@
expect(room_els.length).toBe(1);
item = room_els[0];
expect(item.textContent.trim()).toBe('balcony@chat.shakespeare.lit');
const conv_el = document.querySelector('#conversejs');
conv_el.parentElement.removeChild(conv_el);
done();
}));
......
......@@ -115,6 +115,9 @@ converse.plugins.add('converse-chatboxviews', {
const body = document.querySelector('body');
body.classList.add(`converse-${_converse.view_mode}`);
this.el.classList.add(`converse-${_converse.view_mode}`);
if (_converse.singleton) {
this.el.classList.add(`converse-singleton`);
}
this.render();
},
......
......@@ -73,7 +73,7 @@ converse.plugins.add('converse-controlbox', {
dependencies: ["converse-modal", "converse-chatboxes", "converse-rosterview", "converse-chatview"],
enabled (_converse) {
return _converse.view_mode !== 'embedded';
return !_converse.singleton;
},
overrides: {
......@@ -112,7 +112,7 @@ converse.plugins.add('converse-controlbox', {
validate (attrs, options) {
const { _converse } = this.__super__;
if (attrs.type === _converse.CONTROLBOX_TYPE) {
if (_converse.view_mode === 'embedded') {
if (_converse.view_mode === 'embedded' && _converse.singleton) {
return 'Controlbox not relevant in embedded view mode';
}
return;
......
// Converse.js
// https://conversejs.org
//
// Copyright (c) 2013-2019, the Converse.js developers
// Licensed under the Mozilla Public License (MPLv2)
import "@converse/headless/converse-muc";
import converse from "@converse/headless/converse-core";
const { Backbone, _ } = converse.env;
converse.plugins.add('converse-embedded', {
enabled (_converse) {
return _converse.view_mode === 'embedded';
},
initialize () {
/* The initialize function gets called as soon as the plugin is
* loaded by converse.js's plugin machinery.
*/
this._converse.api.settings.update({
'allow_logout': false, // No point in logging out when we have auto_login as true.
'allow_muc_invitations': false, // Doesn't make sense to allow because only
// roster contacts can be invited
'hide_muc_server': true
});
const { _converse } = this;
if (!Array.isArray(_converse.auto_join_rooms) && !_.isArray(_converse.auto_join_private_chats)) {
throw new Error("converse-embedded: auto_join_rooms must be an Array");
}
if (_converse.auto_join_rooms.length > 1 && _converse.auto_join_private_chats.length > 1) {
throw new Error("converse-embedded: It doesn't make "+
"sense to have the auto_join_rooms setting more then one, "+
"since only one chat room can be open at any time.");
}
}
});
......@@ -14,10 +14,11 @@ import tpl_brand_heading from "templates/inverse_brand_heading.html";
const { Strophe, _ } = converse.env;
converse.plugins.add('converse-fullscreen', {
enabled (_converse) {
return _.includes(['fullscreen', 'embedded'], _converse.view_mode);
return _converse.isUniView();
},
overrides: {
......
......@@ -720,8 +720,8 @@ converse.plugins.add('converse-muc-views', {
*/
return tpl_chatroom_head(
Object.assign(this.model.toJSON(), {
'_converse': _converse,
'Strophe': Strophe,
'_converse': _converse,
'info_close': __('Close and leave this groupchat'),
'info_configure': __('Configure this groupchat'),
'info_details': __('Show more details about this groupchat'),
......
......@@ -7,110 +7,39 @@
/* converse-singleton
* ******************
*
* A plugin which ensures that only one chat (private or groupchat) is
* visible at any one time. All other ongoing chats are hidden and kept in the
* background.
*
* This plugin makes sense in mobile or fullscreen chat environments (as
* configured by the `view_mode` setting).
* A plugin which restricts Converse to only one chat.
*/
import "converse-chatview";
import converse from "@converse/headless/converse-core";
const { _, Strophe } = converse.env;
const u = converse.env.utils;
function hideChat (view) {
if (view.model.get('id') === 'controlbox') { return; }
u.safeSave(view.model, {'hidden': true});
view.hide();
}
converse.plugins.add('converse-singleton', {
// It's possible however to make optional dependencies non-optional.
// If the setting "strict_plugin_dependencies" is set to true,
// an error will be raised if the plugin is not found.
//
// NB: These plugins need to have already been loaded via require.js.
dependencies: ['converse-chatboxes', 'converse-muc', 'converse-muc-views', 'converse-controlbox', 'converse-rosterview'],
overrides: {
// overrides mentioned here will be picked up by converse.js's
// plugin architecture they will replace existing methods on the
// relevant objects or classes.
//
// new functions which don't exist yet can also be added.
ChatBox: {
maybeShow (force) {
// This method must return the chatbox
const { _converse } = this.__super__;
if (!force && _converse.isUniView()) {
if (this.get('id') === 'controlbox') {
return this.trigger('show');
}
const any_chats_visible = _converse.chatboxes
.filter(cb => cb.get('id') != 'controlbox')
.filter(cb => !cb.get('hidden')).length > 0;
if (!any_chats_visible || !this.get('hidden')) {
return this.trigger('show');
}
} else {
return this.__super__.maybeShow.apply(this, arguments);
}
}
},
ChatBoxes: {
createChatBox (jid, attrs) {
/* Make sure new chat boxes are hidden by default. */
const { _converse } = this.__super__;
if (_converse.isUniView()) {
attrs = attrs || {};
attrs.hidden = true;
}
return this.__super__.createChatBox.call(this, jid, attrs);
}
},
ChatBoxView: {
shouldShowOnTextMessage () {
const { _converse } = this.__super__;
if (_converse.isUniView()) {
return false;
} else {
return this.__super__.shouldShowOnTextMessage.apply(this, arguments);
}
},
_show (focus) {
/* We only have one chat visible at any one
* time. So before opening a chat, we make sure all other
* chats are hidden.
*/
const { _converse } = this.__super__;
if (_converse.isUniView()) {
_.each(this.__super__._converse.chatboxviews.xget(this.model.get('id')), hideChat);
u.safeSave(this.model, {'hidden': false});
}
return this.__super__._show.apply(this, arguments);
}
},
ChatRoomView: {
show (focus) {
const { _converse } = this.__super__;
if (_converse.isUniView()) {
_.each(this.__super__._converse.chatboxviews.xget(this.model.get('id')), hideChat);
u.safeSave(this.model, {'hidden': false});
}
return this.__super__.show.apply(this, arguments);
}
enabled (_converse) {
return _converse.singleton;
},
initialize () {
/* The initialize function gets called as soon as the plugin is
* loaded by converse.js's plugin machinery.
*/
this._converse.api.settings.update({
'allow_logout': false, // No point in logging out when we have auto_login as true.
'allow_muc_invitations': false, // Doesn't make sense to allow because only
// roster contacts can be invited
'hide_muc_server': true
});
const { _converse } = this;
if (!_.isArray(_converse.auto_join_rooms) && !_.isArray(_converse.auto_join_private_chats)) {
throw new Error("converse-singleton: auto_join_rooms must be an Array");
}
if (_converse.auto_join_rooms.length > 1 || _converse.auto_join_private_chats.length > 1) {
throw new Error("It doesn't make sense to have singleton set to true and " +
"auto_join_rooms or auto_join_private_chats set to more then one, " +
"since only one chat room may be open at any time.");
}
}
});
// Converse.js
// http://conversejs.org
//
// Copyright (c) 2013-2018, the Converse.js developers
// Licensed under the Mozilla Public License (MPLv2)
/* converse-uniview
* ****************
*
* A plugin which ensures that only one chat (private or groupchat) is
* visible at any one time. All other ongoing chats are hidden and kept in the
* background.
*
* This plugin makes sense in mobile, embedded or fullscreen chat environments
* (as configured by the `view_mode` setting).
*/
import "converse-chatview";
import converse from "@converse/headless/converse-core";
const { _, Strophe } = converse.env;
const u = converse.env.utils;
function hideChat (view) {
if (view.model.get('id') === 'controlbox') { return; }
u.safeSave(view.model, {'hidden': true});
view.hide();
}
function visibleChats (_converse) {
return _converse.chatboxes
.filter(cb => (cb.get('id') !== 'controlbox' && !cb.get('hidden'))).length > 0;
}
converse.plugins.add('converse-uniview', {
// It's possible however to make optional dependencies non-optional.
// If the setting "strict_plugin_dependencies" is set to true,
// an error will be raised if the plugin is not found.
dependencies: ['converse-chatboxes', 'converse-muc-views', 'converse-controlbox', 'converse-rosterview'],
overrides: {
// overrides mentioned here will be picked up by converse.js's
// plugin architecture they will replace existing methods on the
// relevant objects or classes.
//
// new functions which don't exist yet can also be added.
ChatBoxes: {
createChatBox (jid, attrs) {
/* Make sure new chat boxes are hidden by default. */
const { _converse } = this.__super__;
if (_converse.isUniView()) {
attrs = attrs || {};
attrs.hidden = true;
}
return this.__super__.createChatBox.call(this, jid, attrs);
}
},
ChatBox: {
maybeShow () {
const { _converse } = this.__super__;
if (_converse.isUniView() && (!this.get('hidden') || !visibleChats(_converse))) {
return this.trigger("show");
} else {
return this.__super__.maybeShow.apply(this, arguments);
}
}
},
ChatBoxView: {
shouldShowOnTextMessage () {
const { _converse } = this.__super__;
if (_converse.isUniView()) {
return false;
} else {
return this.__super__.shouldShowOnTextMessage.apply(this, arguments);
}
},
_show (focus) {
/* We only have one chat visible at any one
* time. So before opening a chat, we make sure all other
* chats are hidden.
*/
const { _converse } = this.__super__;
if (_converse.isUniView()) {
_.each(this.__super__._converse.chatboxviews.xget(this.model.get('id')), hideChat);
u.safeSave(this.model, {'hidden': false});
}
return this.__super__._show.apply(this, arguments);
}
},
ChatRoomView: {
show (focus) {
const { _converse } = this.__super__;
if (_converse.isUniView()) {
_.each(this.__super__._converse.chatboxviews.xget(this.model.get('id')), hideChat);
u.safeSave(this.model, {'hidden': false});
}
return this.__super__.show.apply(this, arguments);
}
}
}
});
......@@ -14,7 +14,6 @@ import "converse-bookmark-views"; // Views for XEP-0048 Bookmarks
import "converse-chatview"; // Renders standalone chat boxes for single user chat
import "converse-controlbox"; // The control box
import "converse-dragresize"; // Allows chat boxes to be resized by dragging them
import "converse-embedded";
import "converse-fullscreen";
import "converse-headline"; // Support for headline messages
import "converse-mam-views";
......@@ -26,6 +25,8 @@ import "converse-push"; // XEP-0357 Push Notifications
import "converse-register"; // XEP-0077 In-band registration
import "converse-roomslist"; // Show currently open chat rooms
import "converse-rosterview";
import "converse-singleton";
import "converse-uniview";
/* END: Removable components */
import converse from "@converse/headless/converse-core";
......@@ -37,7 +38,6 @@ const WHITELISTED_PLUGINS = [
'converse-chatview',
'converse-controlbox',
'converse-dragresize',
'converse-embedded',
'converse-fullscreen',
'converse-headline',
'converse-mam-views',
......@@ -52,7 +52,8 @@ const WHITELISTED_PLUGINS = [
'converse-register',
'converse-roomslist',
'converse-rosterview',
'converse-singleton'
'converse-singleton',
'converse-uniview'
];
const initialize = converse.initialize;
......
......@@ -369,10 +369,13 @@ converse.plugins.add('converse-chatboxes', {
},
validate (attrs, options) {
const { _converse } = this.__super__;
if (!attrs.jid) {
return 'Ignored ChatBox without JID';
}
const auto_join = _converse.auto_join_private_chats.concat(_converse.auto_join_rooms);
if (_converse.singleton && !_.includes(auto_join, attrs.jid)) {
return "Ignored ChatBox that's not being auto joined in singleton mode";
}
},
getDisplayName () {
......@@ -938,7 +941,8 @@ converse.plugins.add('converse-chatboxes', {
onChatBoxesFetched (collection) {
/* Show chat boxes upon receiving them from sessionStorage */
collection.each(chatbox => chatbox.maybeShow());
collection.filter(c => !c.isValid()).forEach(c => c.destroy());
collection.forEach(c => c.maybeShow());
/**
* Triggered when a message stanza is been received and processed.
* @event _converse#message
......@@ -957,7 +961,7 @@ converse.plugins.add('converse-chatboxes', {
this.registerMessageHandler();
this.fetch({
'add': true,
'success': this.onChatBoxesFetched.bind(this)
'success': c => this.onChatBoxesFetched(c)
});
},
......
......@@ -221,6 +221,7 @@ _converse.default_settings = {
rid: undefined,
root: window.document,
sid: undefined,
singleton: false,
strict_plugin_dependencies: false,
trusted: true,
view_mode: 'overlayed', // Choices are 'overlayed', 'fullscreen', 'mobile'
......@@ -337,7 +338,7 @@ function initPlugins() {
const whitelist = _converse.core_plugins.concat(
_converse.whitelisted_plugins);
if (_converse.view_mode === 'embedded') {
if (_converse.singleton) {
_.forEach([ // eslint-disable-line lodash/prefer-map
"converse-bookmarks",
"converse-controlbox",
......
......@@ -13,9 +13,11 @@
<p class="chatroom-description">{{o.description}}</p>
</div>
<div class="chatbox-buttons row no-gutters">
<a class="chatbox-btn close-chatbox-button fa fa-sign-out-alt" title="{{{o.info_close}}}"></a>
{[ if (!o._converse.singleton) { ]}
<a class="chatbox-btn close-chatbox-button fa fa-sign-out-alt" title="{{{o.info_close}}}"></a>
{[ } ]}
{[ if (o.affiliation == 'owner') { ]}
<a class="chatbox-btn configure-chatroom-button fa fa-wrench" title="{{{o.info_configure}}} "></a>
<a class="chatbox-btn configure-chatroom-button fa fa-wrench" title="{{{o.info_configure}}} "></a>
{[ } ]}
<a class="chatbox-btn show-room-details-modal fa fa-info-circle" title="{{{o.info_details}}}"></a>
</div>
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment