Commit 38d0d836 authored by JC Brand's avatar JC Brand

New config setting `message_limit`

for limiting messages to a certain number of characters.
parent de3099a9
......@@ -20,6 +20,7 @@
- Replace `moment` with [DayJS](https://github.com/iamkun/dayjs).
- New config option [enable_smacks](https://conversejs.org/docs/html/configuration.html#enable-smacks).
- New config option [muc_show_join_leave_status](https://conversejs.org/docs/html/configuration.html#muc-show-join-leave-status)
- New config option [message_limit](https://conversejs.org/docs/html/configuration.html#message-limit)
- 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
......
......@@ -869,6 +869,18 @@ XEP-0280 requires server support, so make sure that message carbons are enabled
on your server.
message_limit
-------------
* Default: ``0``
Determines the allowed amount of characters in a chat message. A value of zero means there is no limit.
Note, this limitation only applies to the Converse UX code running in the browser
and it's trivial for an attacker to bypass this restriction.
You should therefore also configure your XMPP server to limit message sizes.
muc_disable_slash_commands
--------------------------
......
......@@ -330,9 +330,6 @@
.private {
color: #4b7003;
}
.toggle-occupants {
float: right;
}
li {
cursor: pointer;
display: inline-block;
......
......@@ -475,6 +475,63 @@
done();
}));
it("shows the remaining character count if a message_limit is configured",
mock.initConverse(
null, ['rosterGroupsFetched', 'chatBoxesFetched'], {'message_limit': 200},
async function (done, _converse) {
await test_utils.waitForRoster(_converse, 'current', 3);
test_utils.openControlBox();
const contact_jid = mock.cur_names[2].replace(/ /g,'.').toLowerCase() + '@montague.lit';
await test_utils.openChatBoxFor(_converse, contact_jid);
const view = _converse.chatboxviews.get(contact_jid);
const toolbar = view.el.querySelector('.chat-toolbar');
const counter = toolbar.querySelector('.message-limit');
expect(counter.textContent).toBe('200');
view.insertIntoTextArea('hello world');
expect(counter.textContent).toBe('188');
toolbar.querySelector('li.toggle-smiley').click();
await test_utils.waitUntil(() => u.isVisible(view.el.querySelector('.toggle-smiley .emoji-picker-container')));
var picker = view.el.querySelector('.toggle-smiley .emoji-picker-container');
var items = picker.querySelectorAll('.emoji-picker li');
items[0].click()
expect(counter.textContent).toBe('177');
const textarea = view.el.querySelector('.chat-textarea');
const ev = {
target: textarea,
preventDefault: _.noop,
keyCode: 13 // Enter
};
view.onKeyDown(ev);
await new Promise((resolve, reject) => view.once('messageInserted', resolve));
view.onKeyUp(ev);
expect(counter.textContent).toBe('200');
textarea.value = 'hello world';
view.onKeyUp(ev);
expect(counter.textContent).toBe('189');
done();
}));
it("does not show a remaining character count if message_limit is zero",
mock.initConverse(
null, ['rosterGroupsFetched', 'chatBoxesFetched'], {'message_limit': 0},
async function (done, _converse) {
await test_utils.waitForRoster(_converse, 'current', 3);
test_utils.openControlBox();
const contact_jid = mock.cur_names[2].replace(/ /g,'.').toLowerCase() + '@montague.lit';
await test_utils.openChatBoxFor(_converse, contact_jid);
const view = _converse.chatboxviews.get(contact_jid);
const counter = view.el.querySelector('.chat-toolbar .message-limit');
expect(counter).toBe(null);
done();
}));
it("can contain a button for starting a call",
mock.initConverse(
null, ['rosterGroupsFetched', 'chatBoxesFetched'], {},
......
......@@ -57,6 +57,7 @@ converse.plugins.add('converse-chatview', {
_converse.api.settings.update({
'emoji_image_path': twemoji.default.base,
'message_limit': 0,
'show_send_button': false,
'show_toolbar': true,
'time_format': 'HH:mm',
......@@ -334,6 +335,8 @@ converse.plugins.add('converse-chatview', {
'click .upload-file': 'toggleFileUpload',
'input .chat-textarea': 'inputChanged',
'keydown .chat-textarea': 'onKeyDown',
'keyup .chat-textarea': 'onKeyUp',
'paste .chat-textarea': 'onPaste',
'dragover .chat-textarea': 'onDragOver',
'drop .chat-textarea': 'onDrop',
},
......@@ -381,16 +384,15 @@ converse.plugins.add('converse-chatview', {
return this;
},
renderToolbar (toolbar, options) {
renderToolbar () {
if (!_converse.show_toolbar) {
return this;
}
toolbar = toolbar || tpl_toolbar;
options = _.assign(
const options = _.assign(
this.model.toJSON(),
this.getToolbarOptions(options || {})
this.getToolbarOptions()
);
this.el.querySelector('.chat-toolbar').innerHTML = toolbar(options);
this.el.querySelector('.chat-toolbar').innerHTML = tpl_toolbar(options);
this.addSpoilerButton(options);
this.addFileUploadButton();
/**
......@@ -407,6 +409,7 @@ converse.plugins.add('converse-chatview', {
const form_container = this.el.querySelector('.bottom-panel');
form_container.innerHTML = tpl_chatbox_message_form(
Object.assign(this.model.toJSON(), {
'message_limit': _converse.message_limit,
'hint_value': _.get(this.el.querySelector('.spoiler-hint'), 'value'),
'label_message': this.model.get('composing_spoiler') ? __('Hidden message') : __('Message'),
'label_send': __('Send'),
......@@ -467,7 +470,7 @@ converse.plugins.add('converse-chatview', {
this.model.sendFiles(evt.dataTransfer.files);
},
async addFileUploadButton (options) {
async addFileUploadButton () {
if (await _converse.api.disco.supports(Strophe.NS.HTTPUPLOAD, _converse.domain)) {
this.el.querySelector('.chat-toolbar').insertAdjacentHTML(
'beforeend',
......@@ -475,11 +478,13 @@ converse.plugins.add('converse-chatview', {
}
},
/**
* Asynchronously adds a button for writing spoiler
* messages, based on whether the contact's clients support it.
* @private
* @method _converse.ChatBoxView#addSpoilerButton
*/
async addSpoilerButton (options) {
/* Asynchronously adds a button for writing spoiler
* messages, based on whether the contact's client supports
* it.
*/
if (!options.show_spoiler_button || this.model.get('type') === _converse.CHATROOMS_TYPE) {
return;
}
......@@ -516,22 +521,24 @@ converse.plugins.add('converse-chatview', {
return this;
},
getToolbarOptions (options) {
getToolbarOptions () {
let label_toggle_spoiler;
if (this.model.get('composing_spoiler')) {
label_toggle_spoiler = __('Click to write as a normal (non-spoiler) message');
} else {
label_toggle_spoiler = __('Click to write your message as a spoiler');
}
return Object.assign(options || {}, {
return {
'label_clear': __('Clear all messages'),
'tooltip_insert_smiley': __('Insert emojis'),
'tooltip_start_call': __('Start a call'),
'label_message_limit': __('Message characters remaining'),
'label_toggle_spoiler': label_toggle_spoiler,
'message_limit': _converse.message_limit,
'show_call_button': _converse.visible_toolbar_buttons.call,
'show_spoiler_button': _converse.visible_toolbar_buttons.spoiler,
'tooltip_insert_smiley': __('Insert emojis'),
'tooltip_start_call': __('Start a call'),
'use_emoji': _converse.visible_toolbar_buttons.emoji,
});
}
},
async updateAfterMessagesFetched () {
......@@ -905,9 +912,11 @@ converse.plugins.add('converse-chatview', {
async onFormSubmitted (ev) {
ev.preventDefault();
const textarea = this.el.querySelector('.chat-textarea'),
message = textarea.value;
const textarea = this.el.querySelector('.chat-textarea');
const message = textarea.value;
if (_converse.message_limit && message.length > _converse.message_limit) {
return;
}
if (!message.replace(/\s/g, '').length) {
return;
}
......@@ -949,9 +958,39 @@ converse.plugins.add('converse-chatview', {
this.setChatState(_converse.ACTIVE, {'silent': true});
},
updateCharCounter (chars) {
if (_converse.message_limit) {
const message_limit = this.el.querySelector('.message-limit');
const counter = _converse.message_limit - chars.length;
message_limit.textContent = counter;
if (counter < 1) {
u.addClass('error', message_limit);
} else {
u.removeClass('error', message_limit);
}
}
},
onPaste (ev) {
this.updateCharCounter(ev.clipboardData.getData('text/plain'));
},
/**
* Event handler for when a depressed key goes up
* @private
* @method _converse.ChatBoxView#onKeyUp
*/
onKeyUp (ev) {
this.updateCharCounter(ev.target.value);
},
/**
* Event handler for when a key is pressed down in a chat box textarea.
* @private
* @method _converse.ChatBoxView#onKeyDown
* @param { Event } ev
*/
onKeyDown (ev) {
/* Event handler for when a key is pressed in a chat box textarea.
*/
if (ev.ctrlKey) {
// When ctrl is pressed, no chars are entered into the textarea.
return;
......@@ -1101,6 +1140,7 @@ converse.plugins.add('converse-chatview', {
textarea.value = '';
textarea.value = existing+value+' ';
}
this.updateCharCounter(textarea.value);
u.placeCaretAtEnd(textarea);
},
......
......@@ -460,6 +460,7 @@ converse.plugins.add('converse-muc-views', {
'click .upload-file': 'toggleFileUpload',
'keydown .chat-textarea': 'onKeyDown',
'keyup .chat-textarea': 'onKeyUp',
'paste .chat-textarea': 'onPaste',
'input .chat-textarea': 'inputChanged',
'dragover .chat-textarea': 'onDragOver',
'drop .chat-textarea': 'onDrop',
......@@ -583,11 +584,12 @@ converse.plugins.add('converse-muc-views', {
if (this.mention_auto_complete.onKeyDown(ev)) {
return;
}
return _converse.ChatBoxView.prototype.onKeyDown.apply(this, arguments);
return _converse.ChatBoxView.prototype.onKeyDown.call(this, ev);
},
onKeyUp (ev) {
this.mention_auto_complete.evaluate(ev);
return _converse.ChatBoxView.prototype.onKeyUp.call(this, ev);
},
showRoomDetailsModal (ev) {
......
......@@ -8,6 +8,9 @@
<li class="toggle-call fa fa-phone" title="{{{o.label_start_call}}}"></li>
{[ } ]}
{[ if (o.show_occupants_toggle) { ]}
<li class="toggle-occupants fa {[ if (o.hidden_occupants) { ]} fa-angle-double-left {[ } else { ]} fa-angle-double-right {[ } ]}"
<li class="toggle-occupants float-right fa {[ if (o.hidden_occupants) { ]} fa-angle-double-left {[ } else { ]} fa-angle-double-right {[ } ]}"
title="{{{o.label_hide_occupants}}}"></li>
{[ } ]}
{[ if (o.message_limit) { ]}
<li class="message-limit font-weight-bold float-right" title="{{{o.label_message_limit}}}">{{{o.message_limit}}}</li>
{[ } ]}
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