Create converse-message-form component

More work on making bottom panel sub-components declarative

- Handle auto-completion in the converse-muc-message-form element
- Make message limit indicator a component
- Rename template
This commit is contained in:
JC Brand 2021-06-03 19:11:32 +02:00
parent 623deac3ec
commit f3efbba26c
41 changed files with 725 additions and 617 deletions

View File

@ -25,7 +25,6 @@ module.exports = function(config) {
{ pattern: "node_modules/sinon/pkg/sinon.js", type: 'module' }, { pattern: "node_modules/sinon/pkg/sinon.js", type: 'module' },
{ pattern: "spec/mock.js", type: 'module' }, { pattern: "spec/mock.js", type: 'module' },
{ pattern: "spec/emojis.js", type: 'module' },
{ pattern: "spec/protocol.js", type: 'module' }, { pattern: "spec/protocol.js", type: 'module' },
{ pattern: "spec/push.js", type: 'module' }, { pattern: "spec/push.js", type: 'module' },
{ pattern: "spec/user-details-modal.js", type: 'module' }, { pattern: "spec/user-details-modal.js", type: 'module' },
@ -43,6 +42,7 @@ module.exports = function(config) {
{ pattern: "src/plugins/bookmark-views/tests/bookmarks.js", type: 'module' }, { pattern: "src/plugins/bookmark-views/tests/bookmarks.js", type: 'module' },
{ pattern: "src/plugins/chatview/tests/chatbox.js", type: 'module' }, { pattern: "src/plugins/chatview/tests/chatbox.js", type: 'module' },
{ pattern: "src/plugins/chatview/tests/corrections.js", type: 'module' }, { pattern: "src/plugins/chatview/tests/corrections.js", type: 'module' },
{ pattern: "src/plugins/chatview/tests/emojis.js", type: 'module' },
{ pattern: "src/plugins/chatview/tests/http-file-upload.js", type: 'module' }, { pattern: "src/plugins/chatview/tests/http-file-upload.js", type: 'module' },
{ pattern: "src/plugins/chatview/tests/markers.js", type: 'module' }, { pattern: "src/plugins/chatview/tests/markers.js", type: 'module' },
{ pattern: "src/plugins/chatview/tests/me-messages.js", type: 'module' }, { pattern: "src/plugins/chatview/tests/me-messages.js", type: 'module' },

View File

@ -48,8 +48,8 @@ describe("Emojis", function () {
'keyCode': 9, 'keyCode': 9,
'key': 'Tab' 'key': 'Tab'
} }
const bottom_panel = view.querySelector('converse-muc-bottom-panel'); const message_form = view.querySelector('converse-muc-message-form');
bottom_panel.onKeyDown(tab_event); message_form.onKeyDown(tab_event);
await u.waitUntil(() => view.querySelector('converse-emoji-picker .emoji-search')?.value === ':gri'); await u.waitUntil(() => view.querySelector('converse-emoji-picker .emoji-search')?.value === ':gri');
await u.waitUntil(() => sizzle('.emojis-lists__container--search .insert-emoji', view).length === 3, 1000); await u.waitUntil(() => sizzle('.emojis-lists__container--search .insert-emoji', view).length === 3, 1000);
let visible_emojis = sizzle('.emojis-lists__container--search .insert-emoji', view); let visible_emojis = sizzle('.emojis-lists__container--search .insert-emoji', view);
@ -89,7 +89,7 @@ describe("Emojis", function () {
_converse.connection._dataRecv(mock.createRequest(presence)); _converse.connection._dataRecv(mock.createRequest(presence));
textarea.value = ':use'; textarea.value = ':use';
bottom_panel.onKeyDown(tab_event); message_form.onKeyDown(tab_event);
await u.waitUntil(() => u.isVisible(view.querySelector('.emoji-picker__lists'))); await u.waitUntil(() => u.isVisible(view.querySelector('.emoji-picker__lists')));
await u.waitUntil(() => input.value === ':use'); await u.waitUntil(() => input.value === ':use');
visible_emojis = sizzle('.insert-emoji:not(.hidden)', picker); visible_emojis = sizzle('.insert-emoji:not(.hidden)', picker);
@ -115,8 +115,8 @@ describe("Emojis", function () {
'keyCode': 9, 'keyCode': 9,
'key': 'Tab' 'key': 'Tab'
} }
const bottom_panel = view.querySelector('converse-muc-bottom-panel'); const message_form = view.querySelector('converse-muc-message-form');
bottom_panel.onKeyDown(tab_event); message_form.onKeyDown(tab_event);
await u.waitUntil(() => u.isVisible(view.querySelector('.emoji-picker__lists'))); await u.waitUntil(() => u.isVisible(view.querySelector('.emoji-picker__lists')));
const picker = view.querySelector('converse-emoji-picker'); const picker = view.querySelector('converse-emoji-picker');
@ -134,7 +134,7 @@ describe("Emojis", function () {
emoji.click(); emoji.click();
await u.waitUntil(() => textarea.value === ':grinning: '); await u.waitUntil(() => textarea.value === ':grinning: ');
textarea.value = ':grinning: :'; textarea.value = ':grinning: :';
bottom_panel.onKeyDown(tab_event); message_form.onKeyDown(tab_event);
await u.waitUntil(() => input.value === ':'); await u.waitUntil(() => input.value === ':');
input.value = ':grimacing'; input.value = ':grimacing';
@ -167,8 +167,8 @@ describe("Emojis", function () {
'key': 'Tab' 'key': 'Tab'
} }
textarea.value = ':'; textarea.value = ':';
const bottom_panel = view.querySelector('converse-muc-bottom-panel'); const message_form = view.querySelector('converse-muc-message-form');
bottom_panel.onKeyDown(tab_event); message_form.onKeyDown(tab_event);
await u.waitUntil(() => u.isVisible(view.querySelector('.emoji-picker__lists'))); await u.waitUntil(() => u.isVisible(view.querySelector('.emoji-picker__lists')));
const picker = view.querySelector('converse-emoji-picker'); const picker = view.querySelector('converse-emoji-picker');
const input = picker.querySelector('.emoji-search'); const input = picker.querySelector('.emoji-search');
@ -179,7 +179,7 @@ describe("Emojis", function () {
expect(textarea.value).toBe(':100: '); expect(textarea.value).toBe(':100: ');
textarea.value = ':'; textarea.value = ':';
bottom_panel.onKeyDown(tab_event); message_form.onKeyDown(tab_event);
await u.waitUntil(() => u.isVisible(view.querySelector('.emoji-picker__lists'))); await u.waitUntil(() => u.isVisible(view.querySelector('.emoji-picker__lists')));
await u.waitUntil(() => input.value === ':'); await u.waitUntil(() => input.value === ':');
input.dispatchEvent(new KeyboardEvent('keydown', tab_event)); input.dispatchEvent(new KeyboardEvent('keydown', tab_event));
@ -285,8 +285,8 @@ describe("Emojis", function () {
// emojis now renders normally again. // emojis now renders normally again.
const textarea = view.querySelector('textarea.chat-textarea'); const textarea = view.querySelector('textarea.chat-textarea');
textarea.value = ':poop: :innocent:'; textarea.value = ':poop: :innocent:';
const bottom_panel = view.querySelector('converse-chat-bottom-panel'); const message_form = view.querySelector('converse-message-form');
bottom_panel.onKeyDown({ message_form.onKeyDown({
target: textarea, target: textarea,
preventDefault: function preventDefault () {}, preventDefault: function preventDefault () {},
keyCode: 13 // Enter keyCode: 13 // Enter
@ -296,7 +296,7 @@ describe("Emojis", function () {
await u.waitUntil(() => view.querySelector(last_msg_sel).textContent === '💩 😇'); await u.waitUntil(() => view.querySelector(last_msg_sel).textContent === '💩 😇');
expect(textarea.value).toBe(''); expect(textarea.value).toBe('');
bottom_panel.onKeyDown({ message_form.onKeyDown({
target: textarea, target: textarea,
keyCode: 38 // Up arrow keyCode: 38 // Up arrow
}); });
@ -306,7 +306,7 @@ describe("Emojis", function () {
await u.waitUntil(() => u.hasClass('correcting', view.querySelector(sel)), 500); await u.waitUntil(() => u.hasClass('correcting', view.querySelector(sel)), 500);
const edited_text = textarea.value += 'This is no longer an emoji-only message'; const edited_text = textarea.value += 'This is no longer an emoji-only message';
textarea.value = edited_text; textarea.value = edited_text;
bottom_panel.onKeyDown({ message_form.onKeyDown({
target: textarea, target: textarea,
preventDefault: function preventDefault () {}, preventDefault: function preventDefault () {},
keyCode: 13 // Enter keyCode: 13 // Enter
@ -318,7 +318,7 @@ describe("Emojis", function () {
expect(u.hasClass('chat-msg__text--larger', message)).toBe(false); expect(u.hasClass('chat-msg__text--larger', message)).toBe(false);
textarea.value = ':smile: Hello world!'; textarea.value = ':smile: Hello world!';
bottom_panel.onKeyDown({ message_form.onKeyDown({
target: textarea, target: textarea,
preventDefault: function preventDefault () {}, preventDefault: function preventDefault () {},
keyCode: 13 // Enter keyCode: 13 // Enter
@ -326,7 +326,7 @@ describe("Emojis", function () {
await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length === 4); await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length === 4);
textarea.value = ':smile: :smiley: :imp:'; textarea.value = ':smile: :smiley: :imp:';
bottom_panel.onKeyDown({ message_form.onKeyDown({
target: textarea, target: textarea,
preventDefault: function preventDefault () {}, preventDefault: function preventDefault () {},
keyCode: 13 // Enter keyCode: 13 // Enter
@ -367,8 +367,8 @@ describe("Emojis", function () {
const textarea = view.querySelector('textarea.chat-textarea'); const textarea = view.querySelector('textarea.chat-textarea');
textarea.value = ':poop: :innocent:'; textarea.value = ':poop: :innocent:';
const bottom_panel = view.querySelector('converse-chat-bottom-panel'); const message_form = view.querySelector('converse-message-form');
bottom_panel.onKeyDown({ message_form.onKeyDown({
target: textarea, target: textarea,
preventDefault: function preventDefault () {}, preventDefault: function preventDefault () {},
keyCode: 13 // Enter keyCode: 13 // Enter
@ -385,7 +385,7 @@ describe("Emojis", function () {
const sent_stanza = sent_stanzas.filter(s => s.nodeName === 'message').pop(); const sent_stanza = sent_stanzas.filter(s => s.nodeName === 'message').pop();
expect(sent_stanza.querySelector('body').innerHTML).toBe('💩 😇'); expect(sent_stanza.querySelector('body').innerHTML).toBe('💩 😇');
done() done()
})); }));
it("can show custom emojis", it("can show custom emojis",
mock.initConverse( mock.initConverse(
@ -419,8 +419,8 @@ describe("Emojis", function () {
const textarea = view.querySelector('textarea.chat-textarea'); const textarea = view.querySelector('textarea.chat-textarea');
textarea.value = 'Running tests for :converse:'; textarea.value = 'Running tests for :converse:';
const bottom_panel = view.querySelector('converse-chat-bottom-panel'); const message_form = view.querySelector('converse-message-form');
bottom_panel.onKeyDown({ message_form.onKeyDown({
target: textarea, target: textarea,
preventDefault: function preventDefault () {}, preventDefault: function preventDefault () {},
keyCode: 13 // Enter keyCode: 13 // Enter

View File

@ -436,8 +436,8 @@ mock.sendMessage = async function (view, message) {
const promise = new Promise(resolve => view.model.messages.once('rendered', resolve)); const promise = new Promise(resolve => view.model.messages.once('rendered', resolve));
const textarea = await u.waitUntil(() => view.querySelector('.chat-textarea')); const textarea = await u.waitUntil(() => view.querySelector('.chat-textarea'));
textarea.value = message; textarea.value = message;
const bottom_panel = view.querySelector('converse-chat-bottom-panel') || view.querySelector('converse-muc-bottom-panel'); const message_form = view.querySelector('converse-message-form') || view.querySelector('converse-muc-message-form');
bottom_panel.onKeyDown({ message_form.onKeyDown({
target: view.querySelector('textarea.chat-textarea'), target: view.querySelector('textarea.chat-textarea'),
preventDefault: () => {}, preventDefault: () => {},
keyCode: 13 keyCode: 13

View File

@ -64,6 +64,7 @@ const ChatBox = ModelWithContact.extend({
this.presence.on('change:show', item => this.onPresenceChanged(item)); this.presence.on('change:show', item => this.onPresenceChanged(item));
} }
this.on('change:chat_state', this.sendChatState, this); this.on('change:chat_state', this.sendChatState, this);
this.on('change:scrolled', () => !this.get('scrolled') && this.clearUnreadMsgCounter());
await this.fetchMessages(); await this.fetchMessages();
/** /**
@ -198,7 +199,7 @@ const ChatBox = ModelWithContact.extend({
* Queue an incoming `chat` message stanza for processing. * Queue an incoming `chat` message stanza for processing.
* @async * @async
* @private * @private
* @method _converse.ChatRoom#queueMessage * @method _converse.ChatBox#queueMessage
* @param { Promise<MessageAttributes> } attrs - A promise which resolves to the message attributes * @param { Promise<MessageAttributes> } attrs - A promise which resolves to the message attributes
*/ */
queueMessage (attrs) { queueMessage (attrs) {
@ -211,7 +212,7 @@ const ChatBox = ModelWithContact.extend({
/** /**
* @async * @async
* @private * @private
* @method _converse.ChatRoom#onMessage * @method _converse.ChatBox#onMessage
* @param { MessageAttributes } attrs_promse - A promise which resolves to the message attributes. * @param { MessageAttributes } attrs_promse - A promise which resolves to the message attributes.
*/ */
async onMessage (attrs) { async onMessage (attrs) {
@ -681,7 +682,6 @@ const ChatBox = ModelWithContact.extend({
return _converse.connection.send(msg); return _converse.connection.send(msg);
}, },
/** /**
* Finds the last eligible message and then sends a XEP-0333 chat marker for it. * Finds the last eligible message and then sends a XEP-0333 chat marker for it.
* @param { ('received'|'displayed'|'acknowledged') } [type='displayed'] * @param { ('received'|'displayed'|'acknowledged') } [type='displayed']
@ -866,7 +866,7 @@ const ChatBox = ModelWithContact.extend({
* before the collection has been fetched. * before the collection has been fetched.
* @async * @async
* @private * @private
* @method _converse.ChatRoom#queueMessageCreation * @method _converse.ChatBox#queueMessageCreation
* @param { Object } attrs * @param { Object } attrs
*/ */
async createMessage (attrs, options) { async createMessage (attrs, options) {
@ -1029,6 +1029,7 @@ const ChatBox = ModelWithContact.extend({
* Given a newly received {@link _converse.Message} instance, * Given a newly received {@link _converse.Message} instance,
* update the unread counter if necessary. * update the unread counter if necessary.
* @private * @private
* @method _converse.ChatBox#handleUnreadMessage
* @param {_converse.Message} message * @param {_converse.Message} message
*/ */
handleUnreadMessage (message) { handleUnreadMessage (message) {
@ -1064,7 +1065,7 @@ const ChatBox = ModelWithContact.extend({
}, },
isScrolledUp () { isScrolledUp () {
return this.get('scrolled', true); return this.get('scrolled');
} }
}); });

View File

@ -97,6 +97,7 @@ const ChatRoomMixin = {
this.on('change:chat_state', this.sendChatState, this); this.on('change:chat_state', this.sendChatState, this);
this.on('change:hidden', this.onHiddenChange, this); this.on('change:hidden', this.onHiddenChange, this);
this.on('change:scrolled', () => !this.get('scrolled') && this.clearUnreadMsgCounter());
this.on('destroy', this.removeHandlers, this); this.on('destroy', this.removeHandlers, this);
await this.restoreSession(); await this.restoreSession();
@ -2562,7 +2563,9 @@ const ChatRoomMixin = {
} }
}, },
/* Given a newly received message, update the unread counter if necessary. /**
* Given a newly received {@link _converse.Message} instance,
* update the unread counter if necessary.
* @private * @private
* @method _converse.ChatRoom#handleUnreadMessage * @method _converse.ChatRoom#handleUnreadMessage
* @param { XMLElement } - The <messsage> stanza * @param { XMLElement } - The <messsage> stanza
@ -2572,7 +2575,13 @@ const ChatRoomMixin = {
return; return;
} }
if (u.isNewMessage(message)) { if (u.isNewMessage(message)) {
if (this.isHidden()) { if (message.get('sender') === 'me') {
// We remove the "scrolled" flag so that the chat area
// gets scrolled down. We always want to scroll down
// when the user writes a message as opposed to when a
// message is received.
this.model.set('scrolled', false);
} else if (this.isHidden() || this.get('scrolled')) {
const settings = { const settings = {
'num_unread_general': this.get('num_unread_general') + 1 'num_unread_general': this.get('num_unread_general') + 1
}; };

View File

@ -1,85 +1,42 @@
import tpl_chatbox_message_form from './templates/chatbox_message_form.js'; import './message-form.js';
import tpl_toolbar from './templates/toolbar.js'; import debounce from 'lodash-es/debounce';
import tpl_bottom_panel from './templates/bottom-panel.js';
import { ElementView } from '@converse/skeletor/src/element.js'; import { ElementView } from '@converse/skeletor/src/element.js';
import { __ } from 'i18n'; import { _converse, api } from '@converse/headless/core';
import { _converse, api, converse } from '@converse/headless/core'; import { clearMessages } from './utils.js';
import { html, render } from 'lit'; import { render } from 'lit';
import { clearMessages, parseMessageForCommands } from './utils.js';
import './styles/chat-bottom-panel.scss'; import './styles/chat-bottom-panel.scss';
const { u } = converse.env;
export default class ChatBottomPanel extends ElementView { export default class ChatBottomPanel extends ElementView {
events = { events = {
'click .send-button': 'onFormSubmitted', 'click .send-button': 'sendButtonClicked',
'click .toggle-clear': 'clearMessages' 'click .toggle-clear': 'clearMessages'
}; };
async connectedCallback () { async connectedCallback () {
super.connectedCallback(); super.connectedCallback();
this.debouncedRender = debounce(this.render, 100);
this.model = _converse.chatboxes.get(this.getAttribute('jid')); this.model = _converse.chatboxes.get(this.getAttribute('jid'));
this.listenTo(this.model, 'change', o => this.onModelChanged(o.changed));
await this.model.initialized; await this.model.initialized;
this.listenTo(this.model.messages, 'change:correcting', this.onMessageCorrecting); this.listenTo(this.model, 'change:num_unread', this.debouncedRender)
this.listenTo(this.model, 'emoji-picker-autocomplete', this.autocompleteInPicker);
this.addEventListener('focusin', ev => this.emitFocused(ev));
this.addEventListener('focusout', ev => this.emitBlurred(ev));
this.render(); this.render();
} }
onModelChanged (changed) {
if ('composing_spoiler' in changed || 'num_unread' in changed || 'scrolled' in changed) {
this.renderMessageForm();
}
}
render () { render () {
render(html`<div class="message-form-container"></div>`, this); render(tpl_bottom_panel({
this.renderMessageForm(); 'model': this.model,
'viewUnreadMessages': ev => this.viewUnreadMessages(ev)
}), this);
} }
renderToolbar () { sendButtonClicked (ev) {
if (!api.settings.get('show_toolbar')) { this.querySelector('converse-message-form')?.onFormSubmitted(ev);
return this;
}
const options = Object.assign(
{
'model': this.model,
'chatview': _converse.chatboxviews.get(this.getAttribute('jid'))
},
this.model.toJSON(),
this.getToolbarOptions()
);
render(tpl_toolbar(options), this.querySelector('.chat-toolbar'));
/**
* Triggered once the _converse.ChatBoxView's toolbar has been rendered
* @event _converse#renderToolbar
* @type { _converse.ChatBoxView }
* @example _converse.api.listen.on('renderToolbar', this => { ... });
*/
api.trigger('renderToolbar', this);
return this;
}
renderMessageForm () {
const form_container = this.querySelector('.message-form-container');
render(
tpl_chatbox_message_form(
Object.assign(this.model.toJSON(), {
'onDrop': ev => this.onDrop(ev),
'hint_value': this.querySelector('.spoiler-hint')?.value,
'inputChanged': ev => this.inputChanged(ev),
'message_value': this.querySelector('.chat-textarea')?.value,
'onChange': ev => this.updateCharCounter(ev.target.value),
'onKeyDown': ev => this.onKeyDown(ev),
'onKeyUp': ev => this.onKeyUp(ev),
'onPaste': ev => this.onPaste(ev),
'viewUnreadMessages': ev => this.viewUnreadMessages(ev)
})
),
form_container
);
this.addEventListener('focusin', ev => this.emitFocused(ev));
this.addEventListener('focusout', ev => this.emitBlurred(ev));
this.renderToolbar();
} }
viewUnreadMessages (ev) { viewUnreadMessages (ev) {
@ -87,19 +44,6 @@ export default class ChatBottomPanel extends ElementView {
this.model.save({ 'scrolled': false }); this.model.save({ 'scrolled': false });
} }
onMessageCorrecting (message) {
if (message.get('correcting')) {
this.insertIntoTextArea(u.prefixMentions(message), true, true);
} else {
const currently_correcting = this.model.messages.findWhere('correcting');
if (currently_correcting && currently_correcting !== message) {
this.insertIntoTextArea(u.prefixMentions(message), true, true);
} else {
this.insertIntoTextArea('', true, false);
}
}
}
emitFocused (ev) { emitFocused (ev) {
_converse.chatboxviews.get(this.getAttribute('jid'))?.emitFocused(ev); _converse.chatboxviews.get(this.getAttribute('jid'))?.emitFocused(ev);
} }
@ -112,18 +56,6 @@ export default class ChatBottomPanel extends ElementView {
return {}; return {};
} }
inputChanged (ev) { // eslint-disable-line class-methods-use-this
if (ev.target.value) {
const height = ev.target.scrollHeight + 'px';
if (ev.target.style.height != height) {
ev.target.style.height = 'auto';
ev.target.style.height = height;
}
} else {
ev.target.style = '';
}
}
onDrop (evt) { onDrop (evt) {
if (evt.dataTransfer.files.length == 0) { if (evt.dataTransfer.files.length == 0) {
// There are no files to be dropped, so this isnt a file // There are no files to be dropped, so this isnt a file
@ -143,211 +75,19 @@ export default class ChatBottomPanel extends ElementView {
clearMessages(this.model); clearMessages(this.model);
} }
parseMessageForCommands (text) {
return parseMessageForCommands(this.model, text);
}
async onFormSubmitted (ev) {
ev?.preventDefault?.();
const textarea = this.querySelector('.chat-textarea');
const message_text = textarea.value.trim();
if (
(api.settings.get('message_limit') && message_text.length > api.settings.get('message_limit')) ||
!message_text.replace(/\s/g, '').length
) {
return;
}
if (!_converse.connection.authenticated) {
const err_msg = __('Sorry, the connection has been lost, and your message could not be sent');
api.alert('error', __('Error'), err_msg);
api.connection.reconnect();
return;
}
let spoiler_hint,
hint_el = {};
if (this.model.get('composing_spoiler')) {
hint_el = this.querySelector('form.sendXMPPMessage input.spoiler-hint');
spoiler_hint = hint_el.value;
}
u.addClass('disabled', textarea);
textarea.setAttribute('disabled', 'disabled');
this.querySelector('converse-emoji-dropdown')?.hideMenu();
const is_command = this.parseMessageForCommands(message_text);
const message = is_command ? null : await this.model.sendMessage(message_text, spoiler_hint);
if (is_command || message) {
hint_el.value = '';
textarea.value = '';
u.removeClass('correcting', textarea);
textarea.style.height = 'auto';
this.updateCharCounter(textarea.value);
}
if (api.settings.get('view_mode') === 'overlayed') {
// XXX: Chrome flexbug workaround. The .chat-content area
// doesn't resize when the textarea is resized to its original size.
const chatview = _converse.chatboxviews.get(this.getAttribute('jid'));
const msgs_container = chatview.querySelector('.chat-content__messages');
msgs_container.parentElement.style.display = 'none';
}
textarea.removeAttribute('disabled');
u.removeClass('disabled', textarea);
if (api.settings.get('view_mode') === 'overlayed') {
// XXX: Chrome flexbug workaround.
const chatview = _converse.chatboxviews.get(this.getAttribute('jid'));
const msgs_container = chatview.querySelector('.chat-content__messages');
msgs_container.parentElement.style.display = '';
}
// Suppress events, otherwise superfluous CSN gets set
// immediately after the message, causing rate-limiting issues.
this.model.setChatState(_converse.ACTIVE, { 'silent': true });
textarea.focus();
}
/**
* Insert a particular string value into the textarea of this chat box.
* @param {string} value - The value to be inserted.
* @param {(boolean|string)} [replace] - Whether an existing value
* should be replaced. If set to `true`, the entire textarea will
* be replaced with the new value. If set to a string, then only
* that string will be replaced *if* a position is also specified.
* @param {integer} [position] - The end index of the string to be
* replaced with the new value.
*/
insertIntoTextArea (value, replace = false, correcting = false, position) {
const textarea = this.querySelector('.chat-textarea');
if (correcting) {
u.addClass('correcting', textarea);
} else {
u.removeClass('correcting', textarea);
}
if (replace) {
if (position && typeof replace == 'string') {
textarea.value = textarea.value.replace(new RegExp(replace, 'g'), (match, offset) =>
offset == position - replace.length ? value + ' ' : match
);
} else {
textarea.value = value;
}
} else {
let existing = textarea.value;
if (existing && existing[existing.length - 1] !== ' ') {
existing = existing + ' ';
}
textarea.value = existing + value + ' ';
}
const ev = document.createEvent('HTMLEvents');
ev.initEvent('change', false, true);
textarea.dispatchEvent(ev);
u.placeCaretAtEnd(textarea);
}
onEscapePressed (ev) {
ev.preventDefault();
const idx = this.model.messages.findLastIndex('correcting');
const message = idx >= 0 ? this.model.messages.at(idx) : null;
if (message) {
message.save('correcting', false);
}
this.insertIntoTextArea('', true, false);
}
async autocompleteInPicker (input, value) { async autocompleteInPicker (input, value) {
await api.emojis.initialize(); await api.emojis.initialize();
const emoji_dropdown = this.querySelector('converse-emoji-dropdown');
const emoji_picker = this.querySelector('converse-emoji-picker'); const emoji_picker = this.querySelector('converse-emoji-picker');
if (emoji_picker && emoji_dropdown) { if (emoji_picker) {
emoji_picker.model.set({ emoji_picker.model.set({
'ac_position': input.selectionStart, 'ac_position': input.selectionStart,
'autocompleting': value, 'autocompleting': value,
'query': value 'query': value
}); });
emoji_dropdown.showMenu(); const emoji_dropdown = this.querySelector('converse-emoji-dropdown');
return true; emoji_dropdown?.showMenu();
} }
} }
onKeyDown (ev) {
if (ev.ctrlKey) {
// When ctrl is pressed, no chars are entered into the textarea.
return;
}
if (!ev.shiftKey && !ev.altKey && !ev.metaKey) {
if (ev.keyCode === converse.keycodes.TAB) {
const value = u.getCurrentWord(ev.target, null, /(:.*?:)/g);
if (value.startsWith(':') && this.autocompleteInPicker(ev.target, value)) {
ev.preventDefault();
ev.stopPropagation();
}
} else if (ev.keyCode === converse.keycodes.FORWARD_SLASH) {
// Forward slash is used to run commands. Nothing to do here.
return;
} else if (ev.keyCode === converse.keycodes.ESCAPE) {
return this.onEscapePressed(ev, this);
} else if (ev.keyCode === converse.keycodes.ENTER) {
return this.onFormSubmitted(ev);
} else if (ev.keyCode === converse.keycodes.UP_ARROW && !ev.target.selectionEnd) {
const textarea = this.querySelector('.chat-textarea');
if (!textarea.value || u.hasClass('correcting', textarea)) {
return this.model.editEarlierMessage();
}
} else if (
ev.keyCode === converse.keycodes.DOWN_ARROW &&
ev.target.selectionEnd === ev.target.value.length &&
u.hasClass('correcting', this.querySelector('.chat-textarea'))
) {
return this.model.editLaterMessage();
}
}
if (
[
converse.keycodes.SHIFT,
converse.keycodes.META,
converse.keycodes.META_RIGHT,
converse.keycodes.ESCAPE,
converse.keycodes.ALT
].includes(ev.keyCode)
) {
return;
}
if (this.model.get('chat_state') !== _converse.COMPOSING) {
// Set chat state to composing if keyCode is not a forward-slash
// (which would imply an internal command and not a message).
this.model.setChatState(_converse.COMPOSING);
}
}
updateCharCounter (chars) {
if (api.settings.get('message_limit')) {
const message_limit = this.querySelector('.message-limit');
const counter = api.settings.get('message_limit') - chars.length;
message_limit.textContent = counter;
if (counter < 1) {
u.addClass('error', message_limit);
} else {
u.removeClass('error', message_limit);
}
}
}
onKeyUp (ev) {
this.updateCharCounter(ev.target.value);
}
onPaste (ev) {
ev.stopPropagation();
if (ev.clipboardData.files.length !== 0) {
ev.preventDefault();
// Workaround for quirk in at least Firefox 60.7 ESR:
// It seems that pasted files disappear from the event payload after
// the event has finished, which apparently happens during async
// processing in sendFiles(). So we copy the array here.
this.model.sendFiles(Array.from(ev.clipboardData.files));
return;
}
this.updateCharCounter(ev.clipboardData.getData('text/plain'));
}
} }
api.elements.define('converse-chat-bottom-panel', ChatBottomPanel); api.elements.define('converse-chat-bottom-panel', ChatBottomPanel);

View File

@ -0,0 +1,229 @@
import tpl_message_form from './templates/message-form.js';
import { ElementView } from '@converse/skeletor/src/element.js';
import { __ } from 'i18n';
import { _converse, api, converse } from "@converse/headless/core";
import { parseMessageForCommands } from './utils.js';
const { u } = converse.env;
export default class MessageForm extends ElementView {
async connectedCallback () {
super.connectedCallback();
this.model = _converse.chatboxes.get(this.getAttribute('jid'));
await this.model.initialized;
this.listenTo(this.model.messages, 'change:correcting', this.onMessageCorrecting);
this.render();
}
toHTML () {
return tpl_message_form(
Object.assign(this.model.toJSON(), {
'onDrop': ev => this.onDrop(ev),
'hint_value': this.querySelector('.spoiler-hint')?.value,
'message_value': this.querySelector('.chat-textarea')?.value,
'onChange': ev => this.model.set({'draft': ev.target.value}),
'onKeyDown': ev => this.onKeyDown(ev),
'onKeyUp': ev => this.onKeyUp(ev),
'onPaste': ev => this.onPaste(ev),
'viewUnreadMessages': ev => this.viewUnreadMessages(ev)
})
);
}
/**
* Insert a particular string value into the textarea of this chat box.
* @param {string} value - The value to be inserted.
* @param {(boolean|string)} [replace] - Whether an existing value
* should be replaced. If set to `true`, the entire textarea will
* be replaced with the new value. If set to a string, then only
* that string will be replaced *if* a position is also specified.
* @param {integer} [position] - The end index of the string to be
* replaced with the new value.
*/
insertIntoTextArea (value, replace = false, correcting = false, position) {
const textarea = this.querySelector('.chat-textarea');
if (correcting) {
u.addClass('correcting', textarea);
} else {
u.removeClass('correcting', textarea);
}
if (replace) {
if (position && typeof replace == 'string') {
textarea.value = textarea.value.replace(new RegExp(replace, 'g'), (match, offset) =>
offset == position - replace.length ? value + ' ' : match
);
} else {
textarea.value = value;
}
} else {
let existing = textarea.value;
if (existing && existing[existing.length - 1] !== ' ') {
existing = existing + ' ';
}
textarea.value = existing + value + ' ';
}
const ev = document.createEvent('HTMLEvents');
ev.initEvent('change', false, true);
textarea.dispatchEvent(ev);
u.placeCaretAtEnd(textarea);
}
onMessageCorrecting (message) {
if (message.get('correcting')) {
this.insertIntoTextArea(u.prefixMentions(message), true, true);
} else {
const currently_correcting = this.model.messages.findWhere('correcting');
if (currently_correcting && currently_correcting !== message) {
this.insertIntoTextArea(u.prefixMentions(message), true, true);
} else {
this.insertIntoTextArea('', true, false);
}
}
}
onEscapePressed (ev) {
ev.preventDefault();
const idx = this.model.messages.findLastIndex('correcting');
const message = idx >= 0 ? this.model.messages.at(idx) : null;
if (message) {
message.save('correcting', false);
}
this.insertIntoTextArea('', true, false);
}
onPaste (ev) {
ev.stopPropagation();
if (ev.clipboardData.files.length !== 0) {
ev.preventDefault();
// Workaround for quirk in at least Firefox 60.7 ESR:
// It seems that pasted files disappear from the event payload after
// the event has finished, which apparently happens during async
// processing in sendFiles(). So we copy the array here.
this.model.sendFiles(Array.from(ev.clipboardData.files));
return;
}
this.model.set({'draft': ev.clipboardData.getData('text/plain')});
}
onKeyUp (ev) {
this.model.set({'draft': ev.target.value});
}
onKeyDown (ev) {
if (ev.ctrlKey) {
// When ctrl is pressed, no chars are entered into the textarea.
return;
}
if (!ev.shiftKey && !ev.altKey && !ev.metaKey) {
if (ev.keyCode === converse.keycodes.TAB) {
const value = u.getCurrentWord(ev.target, null, /(:.*?:)/g);
if (value.startsWith(':')) {
ev.preventDefault();
ev.stopPropagation();
this.model.trigger('emoji-picker-autocomplete', ev.target, value);
}
} else if (ev.keyCode === converse.keycodes.FORWARD_SLASH) {
// Forward slash is used to run commands. Nothing to do here.
return;
} else if (ev.keyCode === converse.keycodes.ESCAPE) {
return this.onEscapePressed(ev, this);
} else if (ev.keyCode === converse.keycodes.ENTER) {
return this.onFormSubmitted(ev);
} else if (ev.keyCode === converse.keycodes.UP_ARROW && !ev.target.selectionEnd) {
const textarea = this.querySelector('.chat-textarea');
if (!textarea.value || u.hasClass('correcting', textarea)) {
return this.model.editEarlierMessage();
}
} else if (
ev.keyCode === converse.keycodes.DOWN_ARROW &&
ev.target.selectionEnd === ev.target.value.length &&
u.hasClass('correcting', this.querySelector('.chat-textarea'))
) {
return this.model.editLaterMessage();
}
}
if (
[
converse.keycodes.SHIFT,
converse.keycodes.META,
converse.keycodes.META_RIGHT,
converse.keycodes.ESCAPE,
converse.keycodes.ALT
].includes(ev.keyCode)
) {
return;
}
if (this.model.get('chat_state') !== _converse.COMPOSING) {
// Set chat state to composing if keyCode is not a forward-slash
// (which would imply an internal command and not a message).
this.model.setChatState(_converse.COMPOSING);
}
}
parseMessageForCommands (text) {
// Wrap util so that we can override in the MUC message-form component
return parseMessageForCommands(this.model, text);
}
async onFormSubmitted (ev) {
ev?.preventDefault?.();
const textarea = this.querySelector('.chat-textarea');
const message_text = textarea.value.trim();
if (
(api.settings.get('message_limit') && message_text.length > api.settings.get('message_limit')) ||
!message_text.replace(/\s/g, '').length
) {
return;
}
if (!_converse.connection.authenticated) {
const err_msg = __('Sorry, the connection has been lost, and your message could not be sent');
api.alert('error', __('Error'), err_msg);
api.connection.reconnect();
return;
}
let spoiler_hint,
hint_el = {};
if (this.model.get('composing_spoiler')) {
hint_el = this.querySelector('form.sendXMPPMessage input.spoiler-hint');
spoiler_hint = hint_el.value;
}
u.addClass('disabled', textarea);
textarea.setAttribute('disabled', 'disabled');
this.querySelector('converse-emoji-dropdown')?.hideMenu();
const is_command = this.parseMessageForCommands(message_text);
const message = is_command ? null : await this.model.sendMessage(message_text, spoiler_hint);
if (is_command || message) {
hint_el.value = '';
textarea.value = '';
u.removeClass('correcting', textarea);
textarea.style.height = 'auto';
this.model.set({'draft': ''});
}
if (api.settings.get('view_mode') === 'overlayed') {
// XXX: Chrome flexbug workaround. The .chat-content area
// doesn't resize when the textarea is resized to its original size.
const chatview = _converse.chatboxviews.get(this.getAttribute('jid'));
const msgs_container = chatview.querySelector('.chat-content__messages');
msgs_container.parentElement.style.display = 'none';
}
textarea.removeAttribute('disabled');
u.removeClass('disabled', textarea);
if (api.settings.get('view_mode') === 'overlayed') {
// XXX: Chrome flexbug workaround.
const chatview = _converse.chatboxviews.get(this.getAttribute('jid'));
const msgs_container = chatview.querySelector('.chat-content__messages');
msgs_container.parentElement.style.display = '';
}
// Suppress events, otherwise superfluous CSN gets set
// immediately after the message, causing rate-limiting issues.
this.model.setChatState(_converse.ACTIVE, { 'silent': true });
textarea.focus();
}
}
api.elements.define('converse-message-form', MessageForm);

View File

@ -1,4 +1,30 @@
import { __ } from 'i18n';
import { api } from '@converse/headless/core';
import { html } from 'lit';
<div class="bottom-panel">
<div class="message-form-container"></div> export default (o) => {
</div> const unread_msgs = __('You have unread messages');
const message_limit = api.settings.get('message_limit');
const show_call_button = api.settings.get('visible_toolbar_buttons').call;
const show_emoji_button = api.settings.get('visible_toolbar_buttons').emoji;
const show_send_button = api.settings.get('show_send_button');
const show_spoiler_button = api.settings.get('visible_toolbar_buttons').spoiler;
const show_toolbar = api.settings.get('show_toolbar');
return html`
${ o.model.get('scrolled') && o.model.get('num_unread') ?
html`<div class="new-msgs-indicator" @click=${ev => o.viewUnreadMessages(ev)}>▼ ${ unread_msgs } ▼</div>` : '' }
${api.settings.get('show_toolbar') ? html`
<converse-chat-toolbar
class="chat-toolbar no-text-select"
.model=${o.model}
?composing_spoiler="${o.model.get('composing_spoiler')}"
?show_call_button="${show_call_button}"
?show_emoji_button="${show_emoji_button}"
?show_send_button="${show_send_button}"
?show_spoiler_button="${show_spoiler_button}"
?show_toolbar="${show_toolbar}"
message_limit="${message_limit}"></converse-chat-toolbar>` : '' }
<converse-message-form jid="${o.model.get('jid')}"></converse-message-form>
`;
}

View File

@ -0,0 +1,29 @@
import { __ } from 'i18n';
import { api } from "@converse/headless/core";
import { html } from "lit";
import { resetElementHeight } from '../utils.js';
export default (o) => {
const label_message = o.composing_spoiler ? __('Hidden message') : __('Message');
const label_spoiler_hint = __('Optional hint');
const show_send_button = api.settings.get('show_send_button');
return html`
<form class="sendXMPPMessage">
<input type="text" placeholder="${label_spoiler_hint || ''}" value="${o.hint_value || ''}" class="${o.composing_spoiler ? '' : 'hidden'} spoiler-hint"/>
<textarea
autofocus
type="text"
@drop=${o.onDrop}
@input=${resetElementHeight}
@keydown=${o.onKeyDown}
@keyup=${o.onKeyUp}
@paste=${o.onPaste}
@change=${o.onChange}
class="chat-textarea
${ show_send_button ? 'chat-textarea-send-button' : '' }
${ o.composing_spoiler ? 'spoiler' : '' }"
placeholder="${label_message}">${ o.message_value || '' }</textarea>
</form>`;
}

View File

@ -1,28 +0,0 @@
import 'shared/chat/toolbar.js';
import { api } from '@converse/headless/core.js';
import { html } from "lit";
export default (o) => {
const message_limit = api.settings.get('message_limit');
const show_call_button = api.settings.get('visible_toolbar_buttons').call;
const show_emoji_button = api.settings.get('visible_toolbar_buttons').emoji;
const show_send_button = api.settings.get('show_send_button');
const show_spoiler_button = api.settings.get('visible_toolbar_buttons').spoiler;
const show_toolbar = api.settings.get('show_toolbar');
return html`
<converse-chat-toolbar
.chatview=${o.chatview}
.model=${o.model}
?composing_spoiler="${o.composing_spoiler}"
?hidden_occupants="${o.hidden_occupants}"
?is_groupchat="${o.is_groupchat}"
?show_call_button="${show_call_button}"
?show_emoji_button="${show_emoji_button}"
?show_occupants_toggle="${o.show_occupants_toggle}"
?show_send_button="${show_send_button}"
?show_spoiler_button="${show_spoiler_button}"
?show_toolbar="${show_toolbar}"
message_limit="${message_limit}"
></converse-chat-toolbar>
`;
}

View File

@ -59,8 +59,8 @@ describe("Chatboxes", function () {
const textarea = view.querySelector('textarea.chat-textarea'); const textarea = view.querySelector('textarea.chat-textarea');
textarea.value = '/clear'; textarea.value = '/clear';
const bottom_panel = view.querySelector('converse-chat-bottom-panel'); const message_form = view.querySelector('converse-message-form');
bottom_panel.onKeyDown({ message_form.onKeyDown({
target: textarea, target: textarea,
preventDefault: function preventDefault () {}, preventDefault: function preventDefault () {},
keyCode: 13 // Enter keyCode: 13 // Enter
@ -264,14 +264,14 @@ describe("Chatboxes", function () {
const toolbar = view.querySelector('.chat-toolbar'); const toolbar = view.querySelector('.chat-toolbar');
const counter = toolbar.querySelector('.message-limit'); const counter = toolbar.querySelector('.message-limit');
expect(counter.textContent).toBe('200'); expect(counter.textContent).toBe('200');
view.getBottomPanel().insertIntoTextArea('hello world'); view.getMessageForm().insertIntoTextArea('hello world');
expect(counter.textContent).toBe('188'); await u.waitUntil(() => counter.textContent === '188');
toolbar.querySelector('.toggle-emojis').click(); toolbar.querySelector('.toggle-emojis').click();
const picker = await u.waitUntil(() => view.querySelector('.emoji-picker__lists')); const picker = await u.waitUntil(() => view.querySelector('.emoji-picker__lists'));
const item = await u.waitUntil(() => picker.querySelector('.emoji-picker li.insert-emoji a')); const item = await u.waitUntil(() => picker.querySelector('.emoji-picker li.insert-emoji a'));
item.click() item.click()
expect(counter.textContent).toBe('179'); await u.waitUntil(() => counter.textContent === '179');
const textarea = view.querySelector('.chat-textarea'); const textarea = view.querySelector('.chat-textarea');
const ev = { const ev = {
@ -279,15 +279,15 @@ describe("Chatboxes", function () {
preventDefault: function preventDefault () {}, preventDefault: function preventDefault () {},
keyCode: 13 // Enter keyCode: 13 // Enter
}; };
const bottom_panel = view.querySelector('converse-chat-bottom-panel'); const message_form = view.querySelector('converse-message-form');
bottom_panel.onKeyDown(ev); message_form.onKeyDown(ev);
await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length); await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length);
bottom_panel.onKeyUp(ev); message_form.onKeyUp(ev);
expect(counter.textContent).toBe('200'); expect(counter.textContent).toBe('200');
textarea.value = 'hello world'; textarea.value = 'hello world';
bottom_panel.onKeyUp(ev); message_form.onKeyUp(ev);
expect(counter.textContent).toBe('189'); await u.waitUntil(() => counter.textContent === '189');
done(); done();
})); }));
@ -430,8 +430,8 @@ describe("Chatboxes", function () {
spyOn(_converse.connection, 'send'); spyOn(_converse.connection, 'send');
spyOn(_converse.api, "trigger").and.callThrough(); spyOn(_converse.api, "trigger").and.callThrough();
const bottom_panel = view.querySelector('converse-chat-bottom-panel'); const message_form = view.querySelector('converse-message-form');
bottom_panel.onKeyDown({ message_form.onKeyDown({
target: view.querySelector('textarea.chat-textarea'), target: view.querySelector('textarea.chat-textarea'),
keyCode: 1 keyCode: 1
}); });
@ -446,7 +446,7 @@ describe("Chatboxes", function () {
expect(stanza.childNodes[2].tagName).toBe('no-permanent-store'); expect(stanza.childNodes[2].tagName).toBe('no-permanent-store');
// The notification is not sent again // The notification is not sent again
bottom_panel.onKeyDown({ message_form.onKeyDown({
target: view.querySelector('textarea.chat-textarea'), target: view.querySelector('textarea.chat-textarea'),
keyCode: 1 keyCode: 1
}); });
@ -470,8 +470,8 @@ describe("Chatboxes", function () {
expect(view.model.get('chat_state')).toBe('active'); expect(view.model.get('chat_state')).toBe('active');
spyOn(_converse.connection, 'send'); spyOn(_converse.connection, 'send');
spyOn(_converse.api, "trigger").and.callThrough(); spyOn(_converse.api, "trigger").and.callThrough();
const bottom_panel = view.querySelector('converse-chat-bottom-panel'); const message_form = view.querySelector('converse-message-form');
bottom_panel.onKeyDown({ message_form.onKeyDown({
target: view.querySelector('textarea.chat-textarea'), target: view.querySelector('textarea.chat-textarea'),
keyCode: 1 keyCode: 1
}); });
@ -579,8 +579,8 @@ describe("Chatboxes", function () {
const view = _converse.chatboxviews.get(contact_jid); const view = _converse.chatboxviews.get(contact_jid);
spyOn(view.model, 'setChatState').and.callThrough(); spyOn(view.model, 'setChatState').and.callThrough();
expect(view.model.get('chat_state')).toBe('active'); expect(view.model.get('chat_state')).toBe('active');
const bottom_panel = view.querySelector('converse-chat-bottom-panel'); const message_form = view.querySelector('converse-message-form');
bottom_panel.onKeyDown({ message_form.onKeyDown({
target: view.querySelector('textarea.chat-textarea'), target: view.querySelector('textarea.chat-textarea'),
keyCode: 1 keyCode: 1
}); });
@ -612,14 +612,14 @@ describe("Chatboxes", function () {
// Test #359. A paused notification should not be sent // Test #359. A paused notification should not be sent
// out if the user simply types longer than the // out if the user simply types longer than the
// timeout. // timeout.
bottom_panel.onKeyDown({ message_form.onKeyDown({
target: view.querySelector('textarea.chat-textarea'), target: view.querySelector('textarea.chat-textarea'),
keyCode: 1 keyCode: 1
}); });
expect(view.model.setChatState).toHaveBeenCalled(); expect(view.model.setChatState).toHaveBeenCalled();
expect(view.model.get('chat_state')).toBe('composing'); expect(view.model.get('chat_state')).toBe('composing');
bottom_panel.onKeyDown({ message_form.onKeyDown({
target: view.querySelector('textarea.chat-textarea'), target: view.querySelector('textarea.chat-textarea'),
keyCode: 1 keyCode: 1
}); });
@ -718,8 +718,8 @@ describe("Chatboxes", function () {
`</message>`); `</message>`);
const bottom_panel = view.querySelector('converse-chat-bottom-panel'); const message_form = view.querySelector('converse-message-form');
bottom_panel.onKeyDown({ message_form.onKeyDown({
target: view.querySelector('textarea.chat-textarea'), target: view.querySelector('textarea.chat-textarea'),
keyCode: 1 keyCode: 1
}); });
@ -937,10 +937,10 @@ describe("Chatboxes", function () {
await u.waitUntil(() => view.querySelector('.chat-msg')); await u.waitUntil(() => view.querySelector('.chat-msg'));
message = '/clear'; message = '/clear';
const bottom_panel = view.querySelector('converse-chat-bottom-panel'); const message_form = view.querySelector('converse-message-form');
spyOn(window, 'confirm').and.callFake(() => true); spyOn(window, 'confirm').and.callFake(() => true);
view.querySelector('.chat-textarea').value = message; view.querySelector('.chat-textarea').value = message;
bottom_panel.onKeyDown({ message_form.onKeyDown({
target: view.querySelector('textarea.chat-textarea'), target: view.querySelector('textarea.chat-textarea'),
preventDefault: function preventDefault () {}, preventDefault: function preventDefault () {},
keyCode: 13 keyCode: 13
@ -1191,7 +1191,7 @@ describe("Chatboxes", function () {
const view = _converse.chatboxviews.get(sender_jid); const view = _converse.chatboxviews.get(sender_jid);
await u.waitUntil(() => view.model.messages.length); await u.waitUntil(() => view.model.messages.length);
expect(select_msgs_indicator().textContent).toBe('1'); expect(select_msgs_indicator().textContent).toBe('1');
const chat_new_msgs_indicator = view.querySelector('.new-msgs-indicator'); const chat_new_msgs_indicator = await u.waitUntil(() => view.querySelector('.new-msgs-indicator'));
chat_new_msgs_indicator.click(); chat_new_msgs_indicator.click();
await u.waitUntil(() => select_msgs_indicator() === undefined); await u.waitUntil(() => select_msgs_indicator() === undefined);
done(); done();

View File

@ -14,15 +14,15 @@ describe("A Chat Message", function () {
const view = _converse.api.chatviews.get(contact_jid); const view = _converse.api.chatviews.get(contact_jid);
const textarea = view.querySelector('textarea.chat-textarea'); const textarea = view.querySelector('textarea.chat-textarea');
expect(textarea.value).toBe(''); expect(textarea.value).toBe('');
const bottom_panel = view.querySelector('converse-chat-bottom-panel'); const message_form = view.querySelector('converse-message-form');
bottom_panel.onKeyDown({ message_form.onKeyDown({
target: textarea, target: textarea,
keyCode: 38 // Up arrow keyCode: 38 // Up arrow
}); });
expect(textarea.value).toBe(''); expect(textarea.value).toBe('');
textarea.value = 'But soft, what light through yonder airlock breaks?'; textarea.value = 'But soft, what light through yonder airlock breaks?';
bottom_panel.onKeyDown({ message_form.onKeyDown({
target: textarea, target: textarea,
preventDefault: function preventDefault () {}, preventDefault: function preventDefault () {},
keyCode: 13 // Enter keyCode: 13 // Enter
@ -34,7 +34,7 @@ describe("A Chat Message", function () {
const first_msg = view.model.messages.findWhere({'message': 'But soft, what light through yonder airlock breaks?'}); const first_msg = view.model.messages.findWhere({'message': 'But soft, what light through yonder airlock breaks?'});
expect(textarea.value).toBe(''); expect(textarea.value).toBe('');
bottom_panel.onKeyDown({ message_form.onKeyDown({
target: textarea, target: textarea,
keyCode: 38 // Up arrow keyCode: 38 // Up arrow
}); });
@ -46,7 +46,7 @@ describe("A Chat Message", function () {
spyOn(_converse.connection, 'send'); spyOn(_converse.connection, 'send');
let new_text = 'But soft, what light through yonder window breaks?'; let new_text = 'But soft, what light through yonder window breaks?';
textarea.value = new_text; textarea.value = new_text;
bottom_panel.onKeyDown({ message_form.onKeyDown({
target: textarea, target: textarea,
preventDefault: function preventDefault () {}, preventDefault: function preventDefault () {},
keyCode: 13 // Enter keyCode: 13 // Enter
@ -80,7 +80,7 @@ describe("A Chat Message", function () {
// Test that pressing the down arrow cancels message correction // Test that pressing the down arrow cancels message correction
await u.waitUntil(() => textarea.value === '') await u.waitUntil(() => textarea.value === '')
bottom_panel.onKeyDown({ message_form.onKeyDown({
target: textarea, target: textarea,
keyCode: 38 // Up arrow keyCode: 38 // Up arrow
}); });
@ -89,7 +89,7 @@ describe("A Chat Message", function () {
expect(view.querySelectorAll('.chat-msg').length).toBe(1); expect(view.querySelectorAll('.chat-msg').length).toBe(1);
await u.waitUntil(() => u.hasClass('correcting', view.querySelector('.chat-msg')), 500); await u.waitUntil(() => u.hasClass('correcting', view.querySelector('.chat-msg')), 500);
expect(textarea.value).toBe('But soft, what light through yonder window breaks?'); expect(textarea.value).toBe('But soft, what light through yonder window breaks?');
bottom_panel.onKeyDown({ message_form.onKeyDown({
target: textarea, target: textarea,
keyCode: 40 // Down arrow keyCode: 40 // Down arrow
}); });
@ -100,7 +100,7 @@ describe("A Chat Message", function () {
new_text = 'It is the east, and Juliet is the one.'; new_text = 'It is the east, and Juliet is the one.';
textarea.value = new_text; textarea.value = new_text;
bottom_panel.onKeyDown({ message_form.onKeyDown({
target: textarea, target: textarea,
preventDefault: function preventDefault () {}, preventDefault: function preventDefault () {},
keyCode: 13 // Enter keyCode: 13 // Enter
@ -110,14 +110,14 @@ describe("A Chat Message", function () {
expect(view.querySelectorAll('.chat-msg').length).toBe(2); expect(view.querySelectorAll('.chat-msg').length).toBe(2);
textarea.value = 'Arise, fair sun, and kill the envious moon'; textarea.value = 'Arise, fair sun, and kill the envious moon';
bottom_panel.onKeyDown({ message_form.onKeyDown({
target: textarea, target: textarea,
preventDefault: function preventDefault () {}, preventDefault: function preventDefault () {},
keyCode: 13 // Enter keyCode: 13 // Enter
}); });
await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length === 3); await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length === 3);
bottom_panel.onKeyDown({ message_form.onKeyDown({
target: textarea, target: textarea,
keyCode: 38 // Up arrow keyCode: 38 // Up arrow
}); });
@ -129,7 +129,7 @@ describe("A Chat Message", function () {
textarea.selectionEnd = 0; // Happens by pressing up, textarea.selectionEnd = 0; // Happens by pressing up,
// but for some reason not in tests, so we set it manually. // but for some reason not in tests, so we set it manually.
bottom_panel.onKeyDown({ message_form.onKeyDown({
target: textarea, target: textarea,
keyCode: 38 // Up arrow keyCode: 38 // Up arrow
}); });
@ -140,7 +140,7 @@ describe("A Chat Message", function () {
await u.waitUntil(() => u.hasClass('correcting', sizzle('.chat-msg', view)[1]), 500); await u.waitUntil(() => u.hasClass('correcting', sizzle('.chat-msg', view)[1]), 500);
textarea.value = 'It is the east, and Juliet is the sun.'; textarea.value = 'It is the east, and Juliet is the sun.';
bottom_panel.onKeyDown({ message_form.onKeyDown({
target: textarea, target: textarea,
preventDefault: function preventDefault () {}, preventDefault: function preventDefault () {},
keyCode: 13 // Enter keyCode: 13 // Enter
@ -176,8 +176,8 @@ describe("A Chat Message", function () {
const textarea = view.querySelector('textarea.chat-textarea'); const textarea = view.querySelector('textarea.chat-textarea');
textarea.value = 'But soft, what light through yonder airlock breaks?'; textarea.value = 'But soft, what light through yonder airlock breaks?';
const bottom_panel = view.querySelector('converse-chat-bottom-panel'); const message_form = view.querySelector('converse-message-form');
bottom_panel.onKeyDown({ message_form.onKeyDown({
target: textarea, target: textarea,
preventDefault: function preventDefault () {}, preventDefault: function preventDefault () {},
keyCode: 13 // Enter keyCode: 13 // Enter
@ -204,7 +204,7 @@ describe("A Chat Message", function () {
spyOn(_converse.connection, 'send'); spyOn(_converse.connection, 'send');
textarea.value = 'But soft, what light through yonder window breaks?'; textarea.value = 'But soft, what light through yonder window breaks?';
bottom_panel.onKeyDown({ message_form.onKeyDown({
target: textarea, target: textarea,
preventDefault: function preventDefault () {}, preventDefault: function preventDefault () {},
keyCode: 13 // Enter keyCode: 13 // Enter

View File

@ -125,8 +125,8 @@ describe("A XEP-0333 Chat Marker", function () {
const view = _converse.api.chatviews.get(muc_jid); const view = _converse.api.chatviews.get(muc_jid);
const textarea = await u.waitUntil(() => view.querySelector('.chat-textarea')); const textarea = await u.waitUntil(() => view.querySelector('.chat-textarea'));
textarea.value = 'But soft, what light through yonder airlock breaks?'; textarea.value = 'But soft, what light through yonder airlock breaks?';
const bottom_panel = view.querySelector('converse-muc-bottom-panel'); const message_form = view.querySelector('converse-muc-message-form');
bottom_panel.onKeyDown({ message_form.onKeyDown({
target: textarea, target: textarea,
preventDefault: function preventDefault () {}, preventDefault: function preventDefault () {},
keyCode: 13 // Enter keyCode: 13 // Enter

View File

@ -1213,14 +1213,13 @@ describe("A Chat Message", function () {
} }
await Promise.all(promises); await Promise.all(promises);
const indicator_el = view.querySelector('.new-msgs-indicator'); const indicator_el = await u.waitUntil(() => view.querySelector('.new-msgs-indicator'));
expect(u.isVisible(indicator_el)).toBeTruthy();
expect(view.model.get('scrolled')).toBe(true); expect(view.model.get('scrolled')).toBe(true);
expect(view.querySelector('.chat-content').scrollTop).toBe(0); expect(view.querySelector('.chat-content').scrollTop).toBe(0);
indicator_el.click(); indicator_el.click();
expect(u.isVisible(indicator_el)).toBeFalsy(); await u.waitUntil(() => !view.querySelector('.new-msgs-indicator'));
expect(view.model.get('scrolled')).toBe(false); await u.waitUntil(() => !view.model.get('scrolled'));
done(); done();
})); }));

View File

@ -110,8 +110,8 @@ describe("A delivery receipt", function () {
const view = _converse.chatboxviews.get(contact_jid); const view = _converse.chatboxviews.get(contact_jid);
const textarea = view.querySelector('textarea.chat-textarea'); const textarea = view.querySelector('textarea.chat-textarea');
textarea.value = 'But soft, what light through yonder airlock breaks?'; textarea.value = 'But soft, what light through yonder airlock breaks?';
const bottom_panel = view.querySelector('converse-chat-bottom-panel'); const message_form = view.querySelector('converse-message-form');
bottom_panel.onKeyDown({ message_form.onKeyDown({
target: textarea, target: textarea,
preventDefault: function preventDefault () {}, preventDefault: function preventDefault () {},
keyCode: 13 // Enter keyCode: 13 // Enter
@ -132,7 +132,7 @@ describe("A delivery receipt", function () {
// Also handle receipts with type 'chat'. See #1353 // Also handle receipts with type 'chat'. See #1353
spyOn(_converse, 'handleMessageStanza').and.callThrough(); spyOn(_converse, 'handleMessageStanza').and.callThrough();
textarea.value = 'Another message'; textarea.value = 'Another message';
bottom_panel.onKeyDown({ message_form.onKeyDown({
target: textarea, target: textarea,
preventDefault: function preventDefault () {}, preventDefault: function preventDefault () {},
keyCode: 13 // Enter keyCode: 13 // Enter

View File

@ -112,8 +112,8 @@ describe("A spoiler message", function () {
const textarea = view.querySelector('.chat-textarea'); const textarea = view.querySelector('.chat-textarea');
textarea.value = 'This is the spoiler'; textarea.value = 'This is the spoiler';
const bottom_panel = view.querySelector('converse-chat-bottom-panel'); const message_form = view.querySelector('converse-message-form');
bottom_panel.onKeyDown({ message_form.onKeyDown({
target: textarea, target: textarea,
preventDefault: function preventDefault () {}, preventDefault: function preventDefault () {},
keyCode: 13 keyCode: 13
@ -193,8 +193,8 @@ describe("A spoiler message", function () {
const hint_input = view.querySelector('.spoiler-hint'); const hint_input = view.querySelector('.spoiler-hint');
hint_input.value = 'This is the hint'; hint_input.value = 'This is the hint';
const bottom_panel = view.querySelector('converse-chat-bottom-panel'); const message_form = view.querySelector('converse-message-form');
bottom_panel.onKeyDown({ message_form.onKeyDown({
target: textarea, target: textarea,
preventDefault: function preventDefault () {}, preventDefault: function preventDefault () {},
keyCode: 13 keyCode: 13

View File

@ -48,3 +48,15 @@ export function parseMessageForCommands (chat, text) {
} }
} }
} }
export function resetElementHeight (ev) {
if (ev.target.value) {
const height = ev.target.scrollHeight + 'px';
if (ev.target.style.height != height) {
ev.target.style.height = 'auto';
ev.target.style.height = height;
}
} else {
ev.target.style = '';
}
}

View File

@ -141,7 +141,6 @@ export default class ChatView extends BaseChatView {
} }
afterShown () { afterShown () {
this.model.clearUnreadMsgCounter();
this.model.setChatState(_converse.ACTIVE); this.model.setChatState(_converse.ACTIVE);
this.scrollDown(); this.scrollDown();
this.maybeFocus(); this.maybeFocus();

View File

@ -8,7 +8,7 @@ export default (o) => {
return html` return html`
<div class="chatbox-title ${ o.status ? '' : "chatbox-title--no-desc"}"> <div class="chatbox-title ${ o.status ? '' : "chatbox-title--no-desc"}">
<div class="chatbox-title--row"> <div class="chatbox-title--row">
${ (!_converse.api.settings.get("singleton")) ? html`<div class="chatbox-navback"><i class="fa fa-arrow-left"></i></div>` : '' } ${ (!_converse.api.settings.get("singleton")) ? html`<converse-controlbox-navback jid="${o.jid}"></converse-controlbox-navback>` : '' }
<div class="chatbox-title__text" title="${o.jid}">${ o.display_name }</div> <div class="chatbox-title__text" title="${o.jid}">${ o.display_name }</div>
</div> </div>
<div class="chatbox-title__buttons row no-gutters"> <div class="chatbox-title__buttons row no-gutters">

View File

@ -4,7 +4,6 @@ import debounce from 'lodash-es/debounce';
import tpl_muc_bottom_panel from './templates/muc-bottom-panel.js'; import tpl_muc_bottom_panel from './templates/muc-bottom-panel.js';
import { __ } from 'i18n'; import { __ } from 'i18n';
import { _converse, api, converse } from "@converse/headless/core"; import { _converse, api, converse } from "@converse/headless/core";
import { getAutoCompleteListItem, parseMessageForMUCCommands } from './utils.js';
import { render } from 'lit'; import { render } from 'lit';
import './styles/muc-bottom-panel.scss'; import './styles/muc-bottom-panel.scss';
@ -14,15 +13,15 @@ export default class MUCBottomPanel extends BottomPanel {
events = { events = {
'click .hide-occupants': 'hideOccupants', 'click .hide-occupants': 'hideOccupants',
'click .send-button': 'onFormSubmitted', 'click .send-button': 'sendButtonClicked',
} }
async connectedCallback () { async connectedCallback () {
// this.model gets set in the super method and we also wait there for this.model.initialized // this.model gets set in the super method and we also wait there for this.model.initialized
await super.connectedCallback(); await super.connectedCallback();
this.debouncedRender = debounce(this.render, 100); this.debouncedRender = debounce(this.render, 100);
this.listenTo(this.model, 'change:composing_spoiler', this.renderMessageForm);
this.listenTo(this.model, 'change:hidden_occupants', this.debouncedRender); this.listenTo(this.model, 'change:hidden_occupants', this.debouncedRender);
this.listenTo(this.model, 'change:num_unread_general', this.debouncedRender)
this.listenTo(this.model.features, 'change:moderated', this.debouncedRender); this.listenTo(this.model.features, 'change:moderated', this.debouncedRender);
this.listenTo(this.model.occupants, 'add', this.renderIfOwnOccupant) this.listenTo(this.model.occupants, 'add', this.renderIfOwnOccupant)
this.listenTo(this.model.occupants, 'change:role', this.renderIfOwnOccupant); this.listenTo(this.model.occupants, 'change:role', this.renderIfOwnOccupant);
@ -33,17 +32,21 @@ export default class MUCBottomPanel extends BottomPanel {
render () { render () {
const entered = this.model.session.get('connection_status') === converse.ROOMSTATUS.ENTERED; const entered = this.model.session.get('connection_status') === converse.ROOMSTATUS.ENTERED;
const can_edit = entered && !(this.model.features.get('moderated') && this.model.getOwnRole() === 'visitor'); const can_edit = entered && !(this.model.features.get('moderated') && this.model.getOwnRole() === 'visitor');
render(tpl_muc_bottom_panel({ can_edit, entered, 'model': this.model }), this); render(tpl_muc_bottom_panel({
if (entered && can_edit) { can_edit, entered,
this.renderMessageForm(); 'model': this.model,
this.initMentionAutoComplete(); 'viewUnreadMessages': ev => this.viewUnreadMessages(ev)
} }), this);
} }
renderIfOwnOccupant (o) { renderIfOwnOccupant (o) {
(o.get('jid') === _converse.bare_jid) && this.debouncedRender(); (o.get('jid') === _converse.bare_jid) && this.debouncedRender();
} }
sendButtonClicked (ev) {
this.querySelector('converse-message-form')?.onFormSubmitted(ev);
}
getToolbarOptions () { getToolbarOptions () {
return Object.assign(super.getToolbarOptions(), { return Object.assign(super.getToolbarOptions(), {
'is_groupchat': true, 'is_groupchat': true,
@ -52,49 +55,11 @@ export default class MUCBottomPanel extends BottomPanel {
}); });
} }
getAutoCompleteList () {
return this.model.getAllKnownNicknames().map(nick => ({ 'label': nick, 'value': `@${nick}` }));
}
initMentionAutoComplete () {
this.mention_auto_complete = new _converse.AutoComplete(this, {
'auto_first': true,
'auto_evaluate': false,
'min_chars': api.settings.get('muc_mention_autocomplete_min_chars'),
'match_current_word': true,
'list': () => this.getAutoCompleteList(),
'filter':
api.settings.get('muc_mention_autocomplete_filter') == 'contains'
? _converse.FILTER_CONTAINS
: _converse.FILTER_STARTSWITH,
'ac_triggers': ['Tab', '@'],
'include_triggers': [],
'item': getAutoCompleteListItem
});
this.mention_auto_complete.on('suggestion-box-selectcomplete', () => (this.auto_completing = false));
}
hideOccupants (ev) { hideOccupants (ev) {
ev?.preventDefault?.(); ev?.preventDefault?.();
ev?.stopPropagation?.(); ev?.stopPropagation?.();
this.model.save({ 'hidden_occupants': true }); this.model.save({ 'hidden_occupants': true });
} }
onKeyDown (ev) {
if (this.mention_auto_complete.onKeyDown(ev)) {
return;
}
super.onKeyDown(ev);
}
onKeyUp (ev) {
this.mention_auto_complete.evaluate(ev);
super.onKeyUp(ev);
}
parseMessageForCommands (text) {
return parseMessageForMUCCommands(this.model, text);
}
} }
api.elements.define('converse-muc-bottom-panel', MUCBottomPanel); api.elements.define('converse-muc-bottom-panel', MUCBottomPanel);

View File

@ -0,0 +1,70 @@
import MessageForm from 'plugins/chatview/message-form.js';
import tpl_muc_message_form from './templates/message-form.js';
import { _converse, api, converse } from "@converse/headless/core";
import { getAutoCompleteListItem, parseMessageForMUCCommands } from './utils.js';
export default class MUCMessageForm extends MessageForm {
toHTML () {
return tpl_muc_message_form(
Object.assign(this.model.toJSON(), {
'onDrop': ev => this.onDrop(ev),
'hint_value': this.querySelector('.spoiler-hint')?.value,
'message_value': this.querySelector('.chat-textarea')?.value,
'onChange': ev => this.model.set({'draft': ev.target.value}),
'onKeyDown': ev => this.onKeyDown(ev),
'onKeyUp': ev => this.onKeyUp(ev),
'onPaste': ev => this.onPaste(ev),
'viewUnreadMessages': ev => this.viewUnreadMessages(ev)
}));
}
afterRender () {
const entered = this.model.session.get('connection_status') === converse.ROOMSTATUS.ENTERED;
const can_edit = entered && !(this.model.features.get('moderated') && this.model.getOwnRole() === 'visitor');
if (entered && can_edit) {
this.initMentionAutoComplete();
}
}
initMentionAutoComplete () {
this.mention_auto_complete = new _converse.AutoComplete(this, {
'auto_first': true,
'auto_evaluate': false,
'min_chars': api.settings.get('muc_mention_autocomplete_min_chars'),
'match_current_word': true,
'list': () => this.getAutoCompleteList(),
'filter':
api.settings.get('muc_mention_autocomplete_filter') == 'contains'
? _converse.FILTER_CONTAINS
: _converse.FILTER_STARTSWITH,
'ac_triggers': ['Tab', '@'],
'include_triggers': [],
'item': getAutoCompleteListItem
});
this.mention_auto_complete.on('suggestion-box-selectcomplete', () => (this.auto_completing = false));
}
parseMessageForCommands (text) {
return parseMessageForMUCCommands(this.model, text);
}
getAutoCompleteList () {
return this.model.getAllKnownNicknames().map(nick => ({ 'label': nick, 'value': `@${nick}` }));
}
onKeyDown (ev) {
if (this.mention_auto_complete.onKeyDown(ev)) {
return;
}
super.onKeyDown(ev);
}
onKeyUp (ev) {
this.mention_auto_complete.evaluate(ev);
super.onKeyUp(ev);
}
}
api.elements.define('converse-muc-message-form', MUCMessageForm);

View File

@ -53,7 +53,6 @@ export default class MUCView extends BaseChatView {
*/ */
afterShown () { afterShown () {
if (!this.model.get('hidden') && !this.model.get('minimized')) { if (!this.model.get('hidden') && !this.model.get('minimized')) {
this.model.clearUnreadMsgCounter();
this.scrollDown(); this.scrollDown();
} }
} }

View File

@ -159,27 +159,3 @@ converse-muc-destroyed {
} }
} }
} }
@include media-breakpoint-down(sm) {
.conversejs {
converse-chats.converse-mobile,
converse-chats.converse-overlayed,
converse-chats.converse-fullscreen {
.chatbox {
.box-flyout {
.chat-head-chatroom {
.chatbox-navback {
margin-right: 0 !important;
.fa-arrow-left {
&:before {
color: var(--chatroom-head-color);
}
}
}
}
}
}
}
}
}

View File

@ -1,6 +1,7 @@
import { __ } from 'i18n'; import { __ } from 'i18n';
import { api } from "@converse/headless/core"; import { api } from "@converse/headless/core";
import { html } from "lit"; import { html } from "lit";
import { resetElementHeight } from 'plugins/chatview/utils.js';
export default (o) => { export default (o) => {
@ -15,16 +16,14 @@ export default (o) => {
<input type="submit" class="btn btn-primary" name="join" value="Join"/> <input type="submit" class="btn btn-primary" name="join" value="Join"/>
</form> </form>
<form class="sendXMPPMessage"> <form class="sendXMPPMessage">
<span class="chat-toolbar no-text-select"></span>
<input type="text" placeholder="${label_spoiler_hint || ''}" value="${o.hint_value || ''}" class="${o.composing_spoiler ? '' : 'hidden'} spoiler-hint"/> <input type="text" placeholder="${label_spoiler_hint || ''}" value="${o.hint_value || ''}" class="${o.composing_spoiler ? '' : 'hidden'} spoiler-hint"/>
<div class="suggestion-box"> <div class="suggestion-box">
<ul class="suggestion-box__results suggestion-box__results--above" hidden=""></ul> <ul class="suggestion-box__results suggestion-box__results--above" hidden=""></ul>
<textarea <textarea
autofocus autofocus
type="text" type="text"
@drop=${o.onDrop} @drop=${o.onDrop}
@input=${o.inputChanged} @input=${resetElementHeight}
@keydown=${o.onKeyDown} @keydown=${o.onKeyDown}
@keyup=${o.onKeyUp} @keyup=${o.onKeyUp}
@paste=${o.onPaste} @paste=${o.onPaste}

View File

@ -1,19 +1,46 @@
import '../message-form.js';
import 'shared/chat/toolbar.js';
import tpl_muc_nickname_form from './muc-nickname-form.js'; import tpl_muc_nickname_form from './muc-nickname-form.js';
import { __ } from 'i18n'; import { __ } from 'i18n';
import { api, converse } from "@converse/headless/core"; import { api, converse } from "@converse/headless/core";
import { html } from "lit"; import { html } from "lit";
const tpl_can_edit = () => html` const tpl_can_edit = (o) => {
<div class="emoji-picker__container dropup"></div> const message_limit = api.settings.get('message_limit');
<div class="message-form-container">`; const show_call_button = api.settings.get('visible_toolbar_buttons').call;
const show_emoji_button = api.settings.get('visible_toolbar_buttons').emoji;
const show_send_button = api.settings.get('show_send_button');
const show_spoiler_button = api.settings.get('visible_toolbar_buttons').spoiler;
const show_toolbar = api.settings.get('show_toolbar');
return html`
${show_toolbar ? html`
<converse-chat-toolbar
class="chat-toolbar no-text-select"
.model=${o.model}
?composing_spoiler="${o.model.get('composing_spoiler')}"
?hidden_occupants="${o.model.get('hidden_occupants')}"
?is_groupchat="${o.model.get('is_groupchat')}"
?show_call_button="${show_call_button}"
?show_emoji_button="${show_emoji_button}"
?show_occupants_toggle="${o.model.get('show_occupants_toggle')}"
?show_send_button="${show_send_button}"
?show_spoiler_button="${show_spoiler_button}"
?show_toolbar="${show_toolbar}"
message_limit="${message_limit}"></converse-chat-toolbar>` : '' }
<converse-muc-message-form jid=${o.model.get('jid')}></converse-muc-message-form>`;
}
export default (o) => { export default (o) => {
const unread_msgs = __('You have unread messages');
const conn_status = o.model.session.get('connection_status'); const conn_status = o.model.session.get('connection_status');
const i18n_not_allowed = __("You're not allowed to send messages in this room"); const i18n_not_allowed = __("You're not allowed to send messages in this room");
if (conn_status === converse.ROOMSTATUS.ENTERED) { if (conn_status === converse.ROOMSTATUS.ENTERED) {
return (o.can_edit) ? tpl_can_edit() : html`<span class="muc-bottom-panel muc-bottom-panel--muted">${i18n_not_allowed}</span>`; return html`
${ o.model.get('scrolled') && o.model.get('num_unread_general') ?
html`<div class="new-msgs-indicator" @click=${ev => o.viewUnreadMessages(ev)}>▼ ${ unread_msgs } ▼</div>` : '' }
${(o.can_edit) ? tpl_can_edit(o) : html`<span class="muc-bottom-panel muc-bottom-panel--muted">${i18n_not_allowed}</span>`}`;
} else if (conn_status == converse.ROOMSTATUS.NICKNAME_REQUIRED) { } else if (conn_status == converse.ROOMSTATUS.NICKNAME_REQUIRED) {
if (api.settings.get('muc_show_logs_before_join')) { if (api.settings.get('muc_show_logs_before_join')) {
return html`<span class="muc-bottom-panel muc-bottom-panel--nickname">${tpl_muc_nickname_form(o.model)}</span>`; return html`<span class="muc-bottom-panel muc-bottom-panel--nickname">${tpl_muc_nickname_form(o.model)}</span>`;

View File

@ -48,10 +48,10 @@ describe("The nickname autocomplete feature", function () {
'keyCode': 50, 'keyCode': 50,
'key': '@' 'key': '@'
}; };
const bottom_panel = view.querySelector('converse-muc-bottom-panel'); const message_form = view.querySelector('converse-muc-message-form');
bottom_panel.onKeyDown(at_event); message_form.onKeyDown(at_event);
textarea.value = '@'; textarea.value = '@';
bottom_panel.onKeyUp(at_event); message_form.onKeyUp(at_event);
await u.waitUntil(() => view.querySelectorAll('.suggestion-box__results li').length === 4); await u.waitUntil(() => view.querySelectorAll('.suggestion-box__results li').length === 4);
expect(view.querySelector('.suggestion-box__results li:first-child').textContent).toBe('dick'); expect(view.querySelector('.suggestion-box__results li:first-child').textContent).toBe('dick');
@ -102,11 +102,11 @@ describe("The nickname autocomplete feature", function () {
'keyCode': 50, 'keyCode': 50,
'key': '@' 'key': '@'
}; };
const bottom_panel = view.querySelector('converse-muc-bottom-panel'); const message_form = view.querySelector('converse-muc-message-form');
textarea.value = '\n' textarea.value = '\n'
bottom_panel.onKeyDown(at_event); message_form.onKeyDown(at_event);
textarea.value = '\n@'; textarea.value = '\n@';
bottom_panel.onKeyUp(at_event); message_form.onKeyUp(at_event);
await u.waitUntil(() => view.querySelectorAll('.suggestion-box__results li').length === 4); await u.waitUntil(() => view.querySelectorAll('.suggestion-box__results li').length === 4);
expect(view.querySelector('.suggestion-box__results li:first-child').textContent).toBe('dick'); expect(view.querySelector('.suggestion-box__results li:first-child').textContent).toBe('dick');
@ -159,10 +159,10 @@ describe("The nickname autocomplete feature", function () {
'key': '@' 'key': '@'
}; };
textarea.value = '(' textarea.value = '('
const bottom_panel = view.querySelector('converse-muc-bottom-panel'); const message_form = view.querySelector('converse-muc-message-form');
bottom_panel.onKeyDown(at_event); message_form.onKeyDown(at_event);
textarea.value = '(@'; textarea.value = '(@';
bottom_panel.onKeyUp(at_event); message_form.onKeyUp(at_event);
await u.waitUntil(() => view.querySelectorAll('.suggestion-box__results li').length === 4); await u.waitUntil(() => view.querySelectorAll('.suggestion-box__results li').length === 4);
expect(view.querySelector('.suggestion-box__results li:first-child').textContent).toBe('dick'); expect(view.querySelector('.suggestion-box__results li:first-child').textContent).toBe('dick');
@ -201,11 +201,11 @@ describe("The nickname autocomplete feature", function () {
'key': '@' 'key': '@'
}; };
const bottom_panel = view.querySelector('converse-muc-bottom-panel'); const message_form = view.querySelector('converse-muc-message-form');
// Test that results are sorted by query index // Test that results are sorted by query index
bottom_panel.onKeyDown(at_event); message_form.onKeyDown(at_event);
textarea.value = '@ber'; textarea.value = '@ber';
bottom_panel.onKeyUp(at_event); message_form.onKeyUp(at_event);
await u.waitUntil(() => view.querySelectorAll('.suggestion-box__results li').length === 3); await u.waitUntil(() => view.querySelectorAll('.suggestion-box__results li').length === 3);
expect(view.querySelector('.suggestion-box__results li:first-child').textContent).toBe('bernard'); expect(view.querySelector('.suggestion-box__results li:first-child').textContent).toBe('bernard');
expect(view.querySelector('.suggestion-box__results li:nth-child(2)').textContent).toBe('naber'); expect(view.querySelector('.suggestion-box__results li:nth-child(2)').textContent).toBe('naber');
@ -213,7 +213,7 @@ describe("The nickname autocomplete feature", function () {
// Test that when the query index is equal, results should be sorted by length // Test that when the query index is equal, results should be sorted by length
textarea.value = '@jo'; textarea.value = '@jo';
bottom_panel.onKeyUp(at_event); message_form.onKeyUp(at_event);
await u.waitUntil(() => view.querySelectorAll('.suggestion-box__results li').length === 2); await u.waitUntil(() => view.querySelectorAll('.suggestion-box__results li').length === 2);
expect(view.querySelector('.suggestion-box__results li:first-child').textContent).toBe('john'); expect(view.querySelector('.suggestion-box__results li:first-child').textContent).toBe('john');
expect(view.querySelector('.suggestion-box__results li:nth-child(2)').textContent).toBe('jones'); expect(view.querySelector('.suggestion-box__results li:nth-child(2)').textContent).toBe('jones');
@ -250,9 +250,9 @@ describe("The nickname autocomplete feature", function () {
'keyCode': 9, 'keyCode': 9,
'key': 'Tab' 'key': 'Tab'
} }
const bottom_panel = view.querySelector('converse-muc-bottom-panel'); const message_form = view.querySelector('converse-muc-message-form');
bottom_panel.onKeyDown(tab_event); message_form.onKeyDown(tab_event);
bottom_panel.onKeyUp(tab_event); message_form.onKeyUp(tab_event);
await u.waitUntil(() => view.querySelector('.suggestion-box__results').hidden === false); await u.waitUntil(() => view.querySelector('.suggestion-box__results').hidden === false);
expect(view.querySelectorAll('.suggestion-box__results li').length).toBe(1); expect(view.querySelectorAll('.suggestion-box__results li').length).toBe(1);
expect(view.querySelector('.suggestion-box__results li').textContent).toBe('some1'); expect(view.querySelector('.suggestion-box__results li').textContent).toBe('some1');
@ -264,9 +264,9 @@ describe("The nickname autocomplete feature", function () {
} }
for (var i=0; i<3; i++) { for (var i=0; i<3; i++) {
// Press backspace 3 times to remove "som" // Press backspace 3 times to remove "som"
bottom_panel.onKeyDown(backspace_event); message_form.onKeyDown(backspace_event);
textarea.value = textarea.value.slice(0, textarea.value.length-1) textarea.value = textarea.value.slice(0, textarea.value.length-1)
bottom_panel.onKeyUp(backspace_event); message_form.onKeyUp(backspace_event);
} }
await u.waitUntil(() => view.querySelector('.suggestion-box__results').hidden === true); await u.waitUntil(() => view.querySelector('.suggestion-box__results').hidden === true);
@ -283,8 +283,8 @@ describe("The nickname autocomplete feature", function () {
_converse.connection._dataRecv(mock.createRequest(presence)); _converse.connection._dataRecv(mock.createRequest(presence));
textarea.value = "hello s s"; textarea.value = "hello s s";
bottom_panel.onKeyDown(tab_event); message_form.onKeyDown(tab_event);
bottom_panel.onKeyUp(tab_event); message_form.onKeyUp(tab_event);
await u.waitUntil(() => view.querySelector('.suggestion-box__results').hidden === false); await u.waitUntil(() => view.querySelector('.suggestion-box__results').hidden === false);
expect(view.querySelectorAll('.suggestion-box__results li').length).toBe(2); expect(view.querySelectorAll('.suggestion-box__results li').length).toBe(2);
@ -294,13 +294,13 @@ describe("The nickname autocomplete feature", function () {
'stopPropagation': function stopPropagation () {}, 'stopPropagation': function stopPropagation () {},
'keyCode': 38 'keyCode': 38
} }
bottom_panel.onKeyDown(up_arrow_event); message_form.onKeyDown(up_arrow_event);
bottom_panel.onKeyUp(up_arrow_event); message_form.onKeyUp(up_arrow_event);
expect(view.querySelectorAll('.suggestion-box__results li').length).toBe(2); expect(view.querySelectorAll('.suggestion-box__results li').length).toBe(2);
expect(view.querySelector('.suggestion-box__results li[aria-selected="false"]').textContent).toBe('some1'); expect(view.querySelector('.suggestion-box__results li[aria-selected="false"]').textContent).toBe('some1');
expect(view.querySelector('.suggestion-box__results li[aria-selected="true"]').textContent).toBe('some2'); expect(view.querySelector('.suggestion-box__results li[aria-selected="true"]').textContent).toBe('some2');
bottom_panel.onKeyDown({ message_form.onKeyDown({
'target': textarea, 'target': textarea,
'preventDefault': function preventDefault () {}, 'preventDefault': function preventDefault () {},
'stopPropagation': function stopPropagation () {}, 'stopPropagation': function stopPropagation () {},
@ -321,12 +321,12 @@ describe("The nickname autocomplete feature", function () {
}); });
_converse.connection._dataRecv(mock.createRequest(presence)); _converse.connection._dataRecv(mock.createRequest(presence));
textarea.value = "hello z"; textarea.value = "hello z";
bottom_panel.onKeyDown(tab_event); message_form.onKeyDown(tab_event);
bottom_panel.onKeyUp(tab_event); message_form.onKeyUp(tab_event);
await u.waitUntil(() => view.querySelector('.suggestion-box__results').hidden === false); await u.waitUntil(() => view.querySelector('.suggestion-box__results').hidden === false);
bottom_panel.onKeyDown(tab_event); message_form.onKeyDown(tab_event);
bottom_panel.onKeyUp(tab_event); message_form.onKeyUp(tab_event);
await u.waitUntil(() => textarea.value === 'hello @z3r0 '); await u.waitUntil(() => textarea.value === 'hello @z3r0 ');
done(); done();
})); }));
@ -361,10 +361,10 @@ describe("The nickname autocomplete feature", function () {
'keyCode': 8, 'keyCode': 8,
'key': 'Backspace' 'key': 'Backspace'
} }
const bottom_panel = view.querySelector('converse-muc-bottom-panel'); const message_form = view.querySelector('converse-muc-message-form');
bottom_panel.onKeyDown(backspace_event); message_form.onKeyDown(backspace_event);
textarea.value = "hello @some1"; // Mimic backspace textarea.value = "hello @some1"; // Mimic backspace
bottom_panel.onKeyUp(backspace_event); message_form.onKeyUp(backspace_event);
await u.waitUntil(() => view.querySelector('.suggestion-box__results').hidden === false); await u.waitUntil(() => view.querySelector('.suggestion-box__results').hidden === false);
expect(view.querySelectorAll('.suggestion-box__results li').length).toBe(1); expect(view.querySelectorAll('.suggestion-box__results li').length).toBe(1);
expect(view.querySelector('.suggestion-box__results li').textContent).toBe('some1'); expect(view.querySelector('.suggestion-box__results li').textContent).toBe('some1');

View File

@ -169,15 +169,15 @@ describe("A Groupchat Message", function () {
const view = _converse.api.chatviews.get(muc_jid); const view = _converse.api.chatviews.get(muc_jid);
const textarea = await u.waitUntil(() => view.querySelector('textarea.chat-textarea')); const textarea = await u.waitUntil(() => view.querySelector('textarea.chat-textarea'));
expect(textarea.value).toBe(''); expect(textarea.value).toBe('');
const bottom_panel = view.querySelector('converse-muc-bottom-panel'); const message_form = view.querySelector('converse-muc-message-form');
bottom_panel.onKeyDown({ message_form.onKeyDown({
target: textarea, target: textarea,
keyCode: 38 // Up arrow keyCode: 38 // Up arrow
}); });
expect(textarea.value).toBe(''); expect(textarea.value).toBe('');
textarea.value = 'But soft, what light through yonder airlock breaks?'; textarea.value = 'But soft, what light through yonder airlock breaks?';
bottom_panel.onKeyDown({ message_form.onKeyDown({
target: textarea, target: textarea,
preventDefault: function preventDefault () {}, preventDefault: function preventDefault () {},
keyCode: 13 // Enter keyCode: 13 // Enter
@ -188,7 +188,7 @@ describe("A Groupchat Message", function () {
const first_msg = view.model.messages.findWhere({'message': 'But soft, what light through yonder airlock breaks?'}); const first_msg = view.model.messages.findWhere({'message': 'But soft, what light through yonder airlock breaks?'});
expect(textarea.value).toBe(''); expect(textarea.value).toBe('');
bottom_panel.onKeyDown({ message_form.onKeyDown({
target: textarea, target: textarea,
keyCode: 38 // Up arrow keyCode: 38 // Up arrow
}); });
@ -200,7 +200,7 @@ describe("A Groupchat Message", function () {
spyOn(_converse.connection, 'send'); spyOn(_converse.connection, 'send');
const new_text = 'But soft, what light through yonder window breaks?' const new_text = 'But soft, what light through yonder window breaks?'
textarea.value = new_text; textarea.value = new_text;
bottom_panel.onKeyDown({ message_form.onKeyDown({
target: textarea, target: textarea,
preventDefault: function preventDefault () {}, preventDefault: function preventDefault () {},
keyCode: 13 // Enter keyCode: 13 // Enter
@ -245,7 +245,7 @@ describe("A Groupchat Message", function () {
// Test that pressing the down arrow cancels message correction // Test that pressing the down arrow cancels message correction
expect(textarea.value).toBe(''); expect(textarea.value).toBe('');
bottom_panel.onKeyDown({ message_form.onKeyDown({
target: textarea, target: textarea,
keyCode: 38 // Up arrow keyCode: 38 // Up arrow
}); });
@ -254,7 +254,7 @@ describe("A Groupchat Message", function () {
expect(view.querySelectorAll('.chat-msg').length).toBe(2); expect(view.querySelectorAll('.chat-msg').length).toBe(2);
await u.waitUntil(() => u.hasClass('correcting', view.querySelector('.chat-msg')), 500); await u.waitUntil(() => u.hasClass('correcting', view.querySelector('.chat-msg')), 500);
expect(textarea.value).toBe('But soft, what light through yonder window breaks?'); expect(textarea.value).toBe('But soft, what light through yonder window breaks?');
bottom_panel.onKeyDown({ message_form.onKeyDown({
target: textarea, target: textarea,
keyCode: 40 // Down arrow keyCode: 40 // Down arrow
}); });

View File

@ -109,8 +109,8 @@ describe("An incoming groupchat message", function () {
'stopPropagation': function stopPropagation () {}, 'stopPropagation': function stopPropagation () {},
'keyCode': 13 // Enter 'keyCode': 13 // Enter
} }
const bottom_panel = view.querySelector('converse-muc-bottom-panel'); const message_form = view.querySelector('converse-muc-message-form');
bottom_panel.onKeyDown(enter_event); message_form.onKeyDown(enter_event);
await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length); await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length);
await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length); await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length);
@ -363,8 +363,8 @@ describe("A sent groupchat message", function () {
'stopPropagation': function stopPropagation () {}, 'stopPropagation': function stopPropagation () {},
'keyCode': 13 // Enter 'keyCode': 13 // Enter
} }
const bottom_panel = view.querySelector('converse-muc-bottom-panel'); const message_form = view.querySelector('converse-muc-message-form');
bottom_panel.onKeyDown(enter_event); message_form.onKeyDown(enter_event);
await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length); await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length);
const sent_stanzas = _converse.connection.sent_stanzas; const sent_stanzas = _converse.connection.sent_stanzas;
const msg = await u.waitUntil(() => sent_stanzas.filter(s => s.nodeName.toLowerCase() === 'message').pop()); const msg = await u.waitUntil(() => sent_stanzas.filter(s => s.nodeName.toLowerCase() === 'message').pop());
@ -423,8 +423,8 @@ describe("A sent groupchat message", function () {
'stopPropagation': function stopPropagation () {}, 'stopPropagation': function stopPropagation () {},
'keyCode': 13 // Enter 'keyCode': 13 // Enter
} }
const bottom_panel = view.querySelector('converse-muc-bottom-panel'); const message_form = view.querySelector('converse-muc-message-form');
bottom_panel.onKeyDown(enter_event); message_form.onKeyDown(enter_event);
await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length); await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length);
const last_msg_sel = 'converse-chat-message:last-child .chat-msg__text'; const last_msg_sel = 'converse-chat-message:last-child .chat-msg__text';
@ -457,7 +457,7 @@ describe("A sent groupchat message", function () {
await u.waitUntil(() => u.hasClass('correcting', view.querySelector('.chat-msg')), 500); await u.waitUntil(() => u.hasClass('correcting', view.querySelector('.chat-msg')), 500);
textarea.value = 'hello @z3r0 @gibson @sw0rdf1sh, how are you?'; textarea.value = 'hello @z3r0 @gibson @sw0rdf1sh, how are you?';
bottom_panel.onKeyDown(enter_event); message_form.onKeyDown(enter_event);
await u.waitUntil(() => view.querySelector('.chat-msg__text').textContent === await u.waitUntil(() => view.querySelector('.chat-msg__text').textContent ===
'hello z3r0 gibson sw0rdf1sh, how are you?', 500); 'hello z3r0 gibson sw0rdf1sh, how are you?', 500);
@ -507,8 +507,8 @@ describe("A sent groupchat message", function () {
'stopPropagation': function stopPropagation () {}, 'stopPropagation': function stopPropagation () {},
'keyCode': 13 // Enter 'keyCode': 13 // Enter
} }
const bottom_panel = view.querySelector('converse-muc-bottom-panel'); const message_form = view.querySelector('converse-muc-message-form');
bottom_panel.onKeyDown(enter_event); message_form.onKeyDown(enter_event);
await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length); await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length);
const msg = _converse.connection.send.calls.all()[0].args[0]; const msg = _converse.connection.send.calls.all()[0].args[0];
@ -542,8 +542,8 @@ describe("A sent groupchat message", function () {
'stopPropagation': function stopPropagation () {}, 'stopPropagation': function stopPropagation () {},
'keyCode': 13 // Enter 'keyCode': 13 // Enter
} }
const bottom_panel = view.querySelector('converse-muc-bottom-panel'); const message_form = view.querySelector('converse-muc-message-form');
bottom_panel.onKeyDown(enter_event); message_form.onKeyDown(enter_event);
const message = await u.waitUntil(() => view.querySelector('.chat-msg__text')); const message = await u.waitUntil(() => view.querySelector('.chat-msg__text'));
expect(message.innerHTML.replace(/<!-.*?->/g, '')).toEqual( expect(message.innerHTML.replace(/<!-.*?->/g, '')).toEqual(
`Welcome <span class="mention">gibson</span> <span title=":poop:">💩</span> `+ `Welcome <span class="mention">gibson</span> <span title=":poop:">💩</span> `+

View File

@ -11,8 +11,8 @@ async function openModtools (_converse, view) {
const textarea = await u.waitUntil(() => view.querySelector('.chat-textarea')); const textarea = await u.waitUntil(() => view.querySelector('.chat-textarea'));
textarea.value = '/modtools'; textarea.value = '/modtools';
const enter = { 'target': textarea, 'preventDefault': function preventDefault () {}, 'keyCode': 13 }; const enter = { 'target': textarea, 'preventDefault': function preventDefault () {}, 'keyCode': 13 };
const bottom_panel = view.querySelector('converse-muc-bottom-panel'); const message_form = view.querySelector('converse-muc-message-form');
bottom_panel.onKeyDown(enter); message_form.onKeyDown(enter);
const modal = await u.waitUntil(() => _converse.api.modal.get('converse-modtools-modal')); const modal = await u.waitUntil(() => _converse.api.modal.get('converse-modtools-modal'));
await u.waitUntil(() => u.isVisible(modal.el), 1000); await u.waitUntil(() => u.isVisible(modal.el), 1000);
return modal; return modal;
@ -256,8 +256,8 @@ describe("The groupchat moderator tool", function () {
const textarea = await u.waitUntil(() => view.querySelector('.chat-textarea')); const textarea = await u.waitUntil(() => view.querySelector('.chat-textarea'));
textarea.value = '/modtools'; textarea.value = '/modtools';
const enter = { 'target': textarea, 'preventDefault': function preventDefault () {}, 'keyCode': 13 }; const enter = { 'target': textarea, 'preventDefault': function preventDefault () {}, 'keyCode': 13 };
const bottom_panel = view.querySelector('converse-muc-bottom-panel'); const message_form = view.querySelector('converse-muc-message-form');
bottom_panel.onKeyDown(enter); message_form.onKeyDown(enter);
const modal = await u.waitUntil(() => _converse.api.modal.get('converse-modtools-modal')); const modal = await u.waitUntil(() => _converse.api.modal.get('converse-modtools-modal'));
await u.waitUntil(() => u.isVisible(modal.el), 1000); await u.waitUntil(() => u.isVisible(modal.el), 1000);
@ -455,8 +455,8 @@ describe("The groupchat moderator tool", function () {
const textarea = await u.waitUntil(() => view.querySelector('.chat-textarea')); const textarea = await u.waitUntil(() => view.querySelector('.chat-textarea'));
textarea.value = '/modtools'; textarea.value = '/modtools';
const enter = { 'target': textarea, 'preventDefault': function preventDefault () {}, 'keyCode': 13 }; const enter = { 'target': textarea, 'preventDefault': function preventDefault () {}, 'keyCode': 13 };
const bottom_panel = view.querySelector('converse-muc-bottom-panel'); const message_form = view.querySelector('converse-muc-message-form');
bottom_panel.onKeyDown(enter); message_form.onKeyDown(enter);
const modal = await u.waitUntil(() => _converse.api.modal.get('converse-modtools-modal')); const modal = await u.waitUntil(() => _converse.api.modal.get('converse-modtools-modal'));
const occupant = view.model.occupants.findWhere({'jid': _converse.bare_jid}); const occupant = view.model.occupants.findWhere({'jid': _converse.bare_jid});

View File

@ -25,8 +25,8 @@ describe("A Groupchat Message", function () {
'stopPropagation': function stopPropagation () {}, 'stopPropagation': function stopPropagation () {},
'keyCode': 13 // Enter 'keyCode': 13 // Enter
} }
const bottom_panel = view.querySelector('converse-muc-bottom-panel'); const message_form = view.querySelector('converse-muc-message-form');
bottom_panel.onKeyDown(enter_event); message_form.onKeyDown(enter_event);
await new Promise(resolve => view.model.messages.once('rendered', resolve)); await new Promise(resolve => view.model.messages.once('rendered', resolve));
const msg = view.model.messages.at(0); const msg = view.model.messages.at(0);
@ -514,8 +514,8 @@ describe("A Groupchat Message", function () {
const view = _converse.api.chatviews.get(muc_jid); const view = _converse.api.chatviews.get(muc_jid);
const textarea = await u.waitUntil(() => view.querySelector('.chat-textarea')); const textarea = await u.waitUntil(() => view.querySelector('.chat-textarea'));
textarea.value = 'But soft, what light through yonder airlock breaks?'; textarea.value = 'But soft, what light through yonder airlock breaks?';
const bottom_panel = view.querySelector('converse-muc-bottom-panel'); const message_form = view.querySelector('converse-muc-message-form');
bottom_panel.onKeyDown({ message_form.onKeyDown({
target: textarea, target: textarea,
preventDefault: function preventDefault () {}, preventDefault: function preventDefault () {},
keyCode: 13 // Enter keyCode: 13 // Enter
@ -589,8 +589,8 @@ describe("A Groupchat Message", function () {
const view = _converse.api.chatviews.get(muc_jid); const view = _converse.api.chatviews.get(muc_jid);
const textarea = await u.waitUntil(() => view.querySelector('.chat-textarea')); const textarea = await u.waitUntil(() => view.querySelector('.chat-textarea'));
textarea.value = 'But soft, what light through yonder airlock breaks?'; textarea.value = 'But soft, what light through yonder airlock breaks?';
const bottom_panel = view.querySelector('converse-muc-bottom-panel'); const message_form = view.querySelector('converse-muc-message-form');
bottom_panel.onKeyDown({ message_form.onKeyDown({
target: textarea, target: textarea,
preventDefault: function preventDefault () {}, preventDefault: function preventDefault () {},
keyCode: 13 // Enter keyCode: 13 // Enter

View File

@ -15,8 +15,8 @@ describe("Chatrooms", function () {
const view = _converse.chatboxviews.get(muc_jid); const view = _converse.chatboxviews.get(muc_jid);
const textarea = await u.waitUntil(() => view.querySelector('.chat-textarea')); const textarea = await u.waitUntil(() => view.querySelector('.chat-textarea'));
textarea.value = '/register'; textarea.value = '/register';
const bottom_panel = view.querySelector('converse-muc-bottom-panel'); const message_form = view.querySelector('converse-muc-message-form');
bottom_panel.onKeyDown({ message_form.onKeyDown({
target: textarea, target: textarea,
preventDefault: function preventDefault () {}, preventDefault: function preventDefault () {},
keyCode: 13 keyCode: 13

View File

@ -281,8 +281,7 @@ describe("Groupchats", function () {
_converse.connection._dataRecv(mock.createRequest(message)); _converse.connection._dataRecv(mock.createRequest(message));
await u.waitUntil(() => view.model.messages.length); await u.waitUntil(() => view.model.messages.length);
const chat_new_msgs_indicator = view.querySelector('.new-msgs-indicator'); const chat_new_msgs_indicator = await u.waitUntil(() => view.querySelector('.new-msgs-indicator'));
await u.waitUntil(() => u.isVisible(chat_new_msgs_indicator));
chat_new_msgs_indicator.click(); chat_new_msgs_indicator.click();
expect(view.model.get('scrolled')).toBeFalsy(); expect(view.model.get('scrolled')).toBeFalsy();
await u.waitUntil(() => !u.isVisible(chat_new_msgs_indicator)); await u.waitUntil(() => !u.isVisible(chat_new_msgs_indicator));
@ -1895,8 +1894,8 @@ describe("Groupchats", function () {
const text = 'This is a sent message'; const text = 'This is a sent message';
const textarea = await u.waitUntil(() => view.querySelector('.chat-textarea')); const textarea = await u.waitUntil(() => view.querySelector('.chat-textarea'));
textarea.value = text; textarea.value = text;
const bottom_panel = view.querySelector('converse-muc-bottom-panel'); const message_form = view.querySelector('converse-muc-message-form');
bottom_panel.onKeyDown({ message_form.onKeyDown({
target: textarea, target: textarea,
preventDefault: function preventDefault () {}, preventDefault: function preventDefault () {},
keyCode: 13 keyCode: 13
@ -2753,8 +2752,8 @@ describe("Groupchats", function () {
const textarea = await u.waitUntil(() => view.querySelector('.chat-textarea')); const textarea = await u.waitUntil(() => view.querySelector('.chat-textarea'));
const enter = { 'target': textarea, 'preventDefault': function preventDefault () {}, 'keyCode': 13 }; const enter = { 'target': textarea, 'preventDefault': function preventDefault () {}, 'keyCode': 13 };
textarea.value = '/help'; textarea.value = '/help';
const bottom_panel = view.querySelector('converse-muc-bottom-panel'); const message_form = view.querySelector('converse-muc-message-form');
bottom_panel.onKeyDown(enter); message_form.onKeyDown(enter);
await u.waitUntil(() => sizzle('converse-chat-help .chat-info', view).length); await u.waitUntil(() => sizzle('converse-chat-help .chat-info', view).length);
let chat_help_el = view.querySelector('converse-chat-help'); let chat_help_el = view.querySelector('converse-chat-help');
@ -2788,7 +2787,7 @@ describe("Groupchats", function () {
await u.waitUntil(() => view.querySelector('converse-chat-help') === null); await u.waitUntil(() => view.querySelector('converse-chat-help') === null);
textarea.value = '/help'; textarea.value = '/help';
bottom_panel.onKeyDown(enter); message_form.onKeyDown(enter);
chat_help_el = await u.waitUntil(() => view.querySelector('converse-chat-help')); chat_help_el = await u.waitUntil(() => view.querySelector('converse-chat-help'));
info_messages = sizzle('.chat-info', chat_help_el); info_messages = sizzle('.chat-info', chat_help_el);
expect(info_messages.length).toBe(18); expect(info_messages.length).toBe(18);
@ -2803,7 +2802,7 @@ describe("Groupchats", function () {
await u.waitUntil(() => view.querySelector('converse-chat-help') === null); await u.waitUntil(() => view.querySelector('converse-chat-help') === null);
textarea.value = '/help'; textarea.value = '/help';
bottom_panel.onKeyDown(enter); message_form.onKeyDown(enter);
chat_help_el = await u.waitUntil(() => view.querySelector('converse-chat-help')); chat_help_el = await u.waitUntil(() => view.querySelector('converse-chat-help'));
info_messages = sizzle('.chat-info', chat_help_el); info_messages = sizzle('.chat-info', chat_help_el);
expect(info_messages.length).toBe(9); expect(info_messages.length).toBe(9);
@ -2818,7 +2817,7 @@ describe("Groupchats", function () {
// Role changes causes rerender, so we need to get the new textarea // Role changes causes rerender, so we need to get the new textarea
textarea.value = '/help'; textarea.value = '/help';
bottom_panel.onKeyDown(enter); message_form.onKeyDown(enter);
await u.waitUntil(() => view.model.get('show_help_messages')); await u.waitUntil(() => view.model.get('show_help_messages'));
chat_help_el = await u.waitUntil(() => view.querySelector('converse-chat-help')); chat_help_el = await u.waitUntil(() => view.querySelector('converse-chat-help'));
info_messages = sizzle('.chat-info', chat_help_el); info_messages = sizzle('.chat-info', chat_help_el);
@ -2833,7 +2832,7 @@ describe("Groupchats", function () {
await u.waitUntil(() => view.querySelector('converse-chat-help') === null); await u.waitUntil(() => view.querySelector('converse-chat-help') === null);
textarea.value = '/help'; textarea.value = '/help';
bottom_panel.onKeyDown(enter); message_form.onKeyDown(enter);
chat_help_el = await u.waitUntil(() => view.querySelector('converse-chat-help')); chat_help_el = await u.waitUntil(() => view.querySelector('converse-chat-help'));
info_messages = sizzle('.chat-info', chat_help_el); info_messages = sizzle('.chat-info', chat_help_el);
expect(info_messages.length).toBe(7); expect(info_messages.length).toBe(7);
@ -2851,10 +2850,10 @@ describe("Groupchats", function () {
const enter = { 'target': textarea, 'preventDefault': function () {}, 'keyCode': 13 }; const enter = { 'target': textarea, 'preventDefault': function () {}, 'keyCode': 13 };
spyOn(window, 'confirm').and.callFake(() => true); spyOn(window, 'confirm').and.callFake(() => true);
textarea.value = '/clear'; textarea.value = '/clear';
const bottom_panel = view.querySelector('converse-muc-bottom-panel'); const message_form = view.querySelector('converse-muc-message-form');
bottom_panel.onKeyDown(enter); message_form.onKeyDown(enter);
textarea.value = '/help'; textarea.value = '/help';
bottom_panel.onKeyDown(enter); message_form.onKeyDown(enter);
await u.waitUntil(() => sizzle('.chat-info:not(.chat-event)', view).length); await u.waitUntil(() => sizzle('.chat-info:not(.chat-event)', view).length);
const info_messages = sizzle('.chat-info:not(.chat-event)', view); const info_messages = sizzle('.chat-info:not(.chat-event)', view);
@ -2910,8 +2909,8 @@ describe("Groupchats", function () {
// First check that an error message appears when a // First check that an error message appears when a
// non-existent nick is used. // non-existent nick is used.
textarea.value = '/member chris Welcome to the club!'; textarea.value = '/member chris Welcome to the club!';
const bottom_panel = view.querySelector('converse-muc-bottom-panel'); const message_form = view.querySelector('converse-muc-message-form');
bottom_panel.onKeyDown({ message_form.onKeyDown({
target: textarea, target: textarea,
preventDefault: function preventDefault () {}, preventDefault: function preventDefault () {},
keyCode: 13 keyCode: 13
@ -2923,7 +2922,7 @@ describe("Groupchats", function () {
// Now test with an existing nick // Now test with an existing nick
textarea.value = '/member marc Welcome to the club!'; textarea.value = '/member marc Welcome to the club!';
bottom_panel.onKeyDown({ message_form.onKeyDown({
target: textarea, target: textarea,
preventDefault: function preventDefault () {}, preventDefault: function preventDefault () {},
keyCode: 13 keyCode: 13
@ -3030,8 +3029,8 @@ describe("Groupchats", function () {
// Check the alias /topic // Check the alias /topic
const textarea = await u.waitUntil(() => view.querySelector('.chat-textarea')); const textarea = await u.waitUntil(() => view.querySelector('.chat-textarea'));
textarea.value = '/topic This is the groupchat subject'; textarea.value = '/topic This is the groupchat subject';
const bottom_panel = view.querySelector('converse-muc-bottom-panel'); const message_form = view.querySelector('converse-muc-message-form');
bottom_panel.onKeyDown({ message_form.onKeyDown({
target: textarea, target: textarea,
preventDefault: function preventDefault () {}, preventDefault: function preventDefault () {},
keyCode: 13 keyCode: 13
@ -3041,7 +3040,7 @@ describe("Groupchats", function () {
// Check /subject // Check /subject
textarea.value = '/subject This is a new subject'; textarea.value = '/subject This is a new subject';
bottom_panel.onKeyDown({ message_form.onKeyDown({
target: textarea, target: textarea,
preventDefault: function preventDefault () {}, preventDefault: function preventDefault () {},
keyCode: 13 keyCode: 13
@ -3055,7 +3054,7 @@ describe("Groupchats", function () {
// Check case insensitivity // Check case insensitivity
textarea.value = '/Subject This is yet another subject'; textarea.value = '/Subject This is yet another subject';
bottom_panel.onKeyDown({ message_form.onKeyDown({
target: textarea, target: textarea,
preventDefault: function preventDefault () {}, preventDefault: function preventDefault () {},
keyCode: 13 keyCode: 13
@ -3068,7 +3067,7 @@ describe("Groupchats", function () {
// Check unsetting the topic // Check unsetting the topic
textarea.value = '/topic'; textarea.value = '/topic';
bottom_panel.onKeyDown({ message_form.onKeyDown({
target: textarea, target: textarea,
preventDefault: function preventDefault () {}, preventDefault: function preventDefault () {},
keyCode: 13 keyCode: 13
@ -3085,9 +3084,9 @@ describe("Groupchats", function () {
const view = _converse.chatboxviews.get('lounge@montague.lit'); const view = _converse.chatboxviews.get('lounge@montague.lit');
const textarea = await u.waitUntil(() => view.querySelector('.chat-textarea')); const textarea = await u.waitUntil(() => view.querySelector('.chat-textarea'));
textarea.value = '/clear'; textarea.value = '/clear';
const bottom_panel = view.querySelector('converse-muc-bottom-panel');
spyOn(window, 'confirm').and.callFake(() => false); spyOn(window, 'confirm').and.callFake(() => false);
bottom_panel.onKeyDown({ const message_form = view.querySelector('converse-muc-message-form');
message_form.onKeyDown({
target: textarea, target: textarea,
preventDefault: function preventDefault () {}, preventDefault: function preventDefault () {},
keyCode: 13 keyCode: 13
@ -3123,8 +3122,8 @@ describe("Groupchats", function () {
const textarea = await u.waitUntil(() => view.querySelector('.chat-textarea')); const textarea = await u.waitUntil(() => view.querySelector('.chat-textarea'));
textarea.value = '/owner'; textarea.value = '/owner';
const bottom_panel = view.querySelector('converse-muc-bottom-panel'); const message_form = view.querySelector('converse-muc-message-form');
bottom_panel.onKeyDown({ message_form.onKeyDown({
target: textarea, target: textarea,
preventDefault: function preventDefault () {}, preventDefault: function preventDefault () {},
keyCode: 13 keyCode: 13
@ -3142,7 +3141,7 @@ describe("Groupchats", function () {
// again via triggering Event doesn't work for some weird // again via triggering Event doesn't work for some weird
// reason. // reason.
textarea.value = '/owner nobody You\'re responsible'; textarea.value = '/owner nobody You\'re responsible';
bottom_panel.onFormSubmitted(new Event('submit')); message_form.onFormSubmitted(new Event('submit'));
await u.waitUntil(() => view.querySelectorAll('.chat-error').length === 2); await u.waitUntil(() => view.querySelectorAll('.chat-error').length === 2);
expect(Array.from(view.querySelectorAll('.chat-error')).pop().textContent.trim()).toBe( expect(Array.from(view.querySelectorAll('.chat-error')).pop().textContent.trim()).toBe(
"Error: couldn't find a groupchat participant based on your arguments"); "Error: couldn't find a groupchat participant based on your arguments");
@ -3154,7 +3153,7 @@ describe("Groupchats", function () {
// again via triggering Event doesn't work for some weird // again via triggering Event doesn't work for some weird
// reason. // reason.
textarea.value = '/owner annoyingGuy You\'re responsible'; textarea.value = '/owner annoyingGuy You\'re responsible';
bottom_panel.onFormSubmitted(new Event('submit')); message_form.onFormSubmitted(new Event('submit'));
expect(view.model.validateRoleOrAffiliationChangeArgs.calls.count()).toBe(3); expect(view.model.validateRoleOrAffiliationChangeArgs.calls.count()).toBe(3);
// Check that the member list now gets updated // Check that the member list now gets updated
@ -3213,8 +3212,8 @@ describe("Groupchats", function () {
const textarea = await u.waitUntil(() => view.querySelector('.chat-textarea')); const textarea = await u.waitUntil(() => view.querySelector('.chat-textarea'));
textarea.value = '/ban'; textarea.value = '/ban';
const bottom_panel = view.querySelector('converse-muc-bottom-panel'); const message_form = view.querySelector('converse-muc-message-form');
bottom_panel.onKeyDown({ message_form.onKeyDown({
target: textarea, target: textarea,
preventDefault: function preventDefault () {}, preventDefault: function preventDefault () {},
keyCode: 13 keyCode: 13
@ -3232,7 +3231,7 @@ describe("Groupchats", function () {
// again via triggering Event doesn't work for some weird // again via triggering Event doesn't work for some weird
// reason. // reason.
textarea.value = '/ban annoyingGuy You\'re annoying'; textarea.value = '/ban annoyingGuy You\'re annoying';
bottom_panel.onFormSubmitted(new Event('submit')); message_form.onFormSubmitted(new Event('submit'));
expect(view.model.validateRoleOrAffiliationChangeArgs.calls.count()).toBe(2); expect(view.model.validateRoleOrAffiliationChangeArgs.calls.count()).toBe(2);
// Check that the member list now gets updated // Check that the member list now gets updated
@ -3277,7 +3276,7 @@ describe("Groupchats", function () {
_converse.connection._dataRecv(mock.createRequest(presence)); _converse.connection._dataRecv(mock.createRequest(presence));
textarea.value = '/ban joe22'; textarea.value = '/ban joe22';
bottom_panel.onFormSubmitted(new Event('submit')); message_form.onFormSubmitted(new Event('submit'));
await u.waitUntil(() => view.querySelector('converse-chat-message:last-child')?.textContent?.trim() === await u.waitUntil(() => view.querySelector('converse-chat-message:last-child')?.textContent?.trim() ===
"Error: couldn't find a groupchat participant based on your arguments"); "Error: couldn't find a groupchat participant based on your arguments");
done(); done();
@ -3313,8 +3312,8 @@ describe("Groupchats", function () {
const textarea = await u.waitUntil(() => view.querySelector('.chat-textarea')); const textarea = await u.waitUntil(() => view.querySelector('.chat-textarea'));
textarea.value = '/kick'; textarea.value = '/kick';
const bottom_panel = view.querySelector('converse-muc-bottom-panel'); const message_form = view.querySelector('converse-muc-message-form');
bottom_panel.onKeyDown({ message_form.onKeyDown({
target: textarea, target: textarea,
preventDefault: function preventDefault () {}, preventDefault: function preventDefault () {},
keyCode: 13 keyCode: 13
@ -3328,7 +3327,7 @@ describe("Groupchats", function () {
// again via triggering Event doesn't work for some weird // again via triggering Event doesn't work for some weird
// reason. // reason.
textarea.value = '/kick @annoying guy You\'re annoying'; textarea.value = '/kick @annoying guy You\'re annoying';
bottom_panel.onFormSubmitted(new Event('submit')); message_form.onFormSubmitted(new Event('submit'));
expect(view.model.validateRoleOrAffiliationChangeArgs.calls.count()).toBe(2); expect(view.model.validateRoleOrAffiliationChangeArgs.calls.count()).toBe(2);
expect(view.model.setRole).toHaveBeenCalled(); expect(view.model.setRole).toHaveBeenCalled();
@ -3415,8 +3414,8 @@ describe("Groupchats", function () {
const textarea = await u.waitUntil(() => view.querySelector('.chat-textarea')); const textarea = await u.waitUntil(() => view.querySelector('.chat-textarea'));
textarea.value = '/op'; textarea.value = '/op';
const bottom_panel = view.querySelector('converse-muc-bottom-panel'); const message_form = view.querySelector('converse-muc-message-form');
bottom_panel.onKeyDown({ message_form.onKeyDown({
target: textarea, target: textarea,
preventDefault: function preventDefault () {}, preventDefault: function preventDefault () {},
keyCode: 13 keyCode: 13
@ -3432,7 +3431,7 @@ describe("Groupchats", function () {
// again via triggering Event doesn't work for some weird // again via triggering Event doesn't work for some weird
// reason. // reason.
textarea.value = '/op trustworthyguy You\'re trustworthy'; textarea.value = '/op trustworthyguy You\'re trustworthy';
bottom_panel.onFormSubmitted(new Event('submit')); message_form.onFormSubmitted(new Event('submit'));
expect(view.model.validateRoleOrAffiliationChangeArgs.calls.count()).toBe(2); expect(view.model.validateRoleOrAffiliationChangeArgs.calls.count()).toBe(2);
expect(view.model.setRole).toHaveBeenCalled(); expect(view.model.setRole).toHaveBeenCalled();
@ -3476,7 +3475,7 @@ describe("Groupchats", function () {
// again via triggering Event doesn't work for some weird // again via triggering Event doesn't work for some weird
// reason. // reason.
textarea.value = '/deop trustworthyguy Perhaps not'; textarea.value = '/deop trustworthyguy Perhaps not';
bottom_panel.onFormSubmitted(new Event('submit')); message_form.onFormSubmitted(new Event('submit'));
expect(view.model.validateRoleOrAffiliationChangeArgs.calls.count()).toBe(3); expect(view.model.validateRoleOrAffiliationChangeArgs.calls.count()).toBe(3);
expect(view.model.setRole).toHaveBeenCalled(); expect(view.model.setRole).toHaveBeenCalled();
@ -3555,8 +3554,8 @@ describe("Groupchats", function () {
const textarea = await u.waitUntil(() => view.querySelector('.chat-textarea')); const textarea = await u.waitUntil(() => view.querySelector('.chat-textarea'));
textarea.value = '/mute'; textarea.value = '/mute';
const bottom_panel = view.querySelector('converse-muc-bottom-panel'); const message_form = view.querySelector('converse-muc-message-form');
bottom_panel.onKeyDown({ message_form.onKeyDown({
target: textarea, target: textarea,
preventDefault: function preventDefault () {}, preventDefault: function preventDefault () {},
keyCode: 13 keyCode: 13
@ -3571,7 +3570,7 @@ describe("Groupchats", function () {
// again via triggering Event doesn't work for some weird // again via triggering Event doesn't work for some weird
// reason. // reason.
textarea.value = '/mute annoyingGuy You\'re annoying'; textarea.value = '/mute annoyingGuy You\'re annoying';
bottom_panel.onFormSubmitted(new Event('submit')); message_form.onFormSubmitted(new Event('submit'));
expect(view.model.validateRoleOrAffiliationChangeArgs.calls.count()).toBe(2); expect(view.model.validateRoleOrAffiliationChangeArgs.calls.count()).toBe(2);
expect(view.model.setRole).toHaveBeenCalled(); expect(view.model.setRole).toHaveBeenCalled();
@ -3612,7 +3611,7 @@ describe("Groupchats", function () {
// again via triggering Event doesn't work for some weird // again via triggering Event doesn't work for some weird
// reason. // reason.
textarea.value = '/voice annoyingGuy Now you can talk again'; textarea.value = '/voice annoyingGuy Now you can talk again';
bottom_panel.onFormSubmitted(new Event('submit')); message_form.onFormSubmitted(new Event('submit'));
expect(view.model.validateRoleOrAffiliationChangeArgs.calls.count()).toBe(3); expect(view.model.validateRoleOrAffiliationChangeArgs.calls.count()).toBe(3);
expect(view.model.setRole).toHaveBeenCalled(); expect(view.model.setRole).toHaveBeenCalled();
@ -3660,8 +3659,8 @@ describe("Groupchats", function () {
spyOn(_converse.api, 'confirm').and.callThrough(); spyOn(_converse.api, 'confirm').and.callThrough();
let textarea = await u.waitUntil(() => view.querySelector('.chat-textarea')); let textarea = await u.waitUntil(() => view.querySelector('.chat-textarea'));
textarea.value = '/destroy'; textarea.value = '/destroy';
let bottom_panel = view.querySelector('converse-muc-bottom-panel'); let message_form = view.querySelector('converse-muc-message-form');
bottom_panel.onFormSubmitted(new Event('submit')); message_form.onFormSubmitted(new Event('submit'));
let modal = await u.waitUntil(() => document.querySelector('.modal-dialog')); let modal = await u.waitUntil(() => document.querySelector('.modal-dialog'));
await u.waitUntil(() => u.isVisible(modal)); await u.waitUntil(() => u.isVisible(modal));
@ -3711,8 +3710,8 @@ describe("Groupchats", function () {
view = _converse.api.chatviews.get(new_muc_jid); view = _converse.api.chatviews.get(new_muc_jid);
textarea = await u.waitUntil(() => view.querySelector('.chat-textarea')); textarea = await u.waitUntil(() => view.querySelector('.chat-textarea'));
textarea.value = '/destroy'; textarea.value = '/destroy';
bottom_panel = view.querySelector('converse-muc-bottom-panel'); message_form = view.querySelector('converse-muc-message-form');
bottom_panel.onFormSubmitted(new Event('submit')); message_form.onFormSubmitted(new Event('submit'));
modal = await u.waitUntil(() => document.querySelector('.modal-dialog')); modal = await u.waitUntil(() => document.querySelector('.modal-dialog'));
await u.waitUntil(() => u.isVisible(modal)); await u.waitUntil(() => u.isVisible(modal));
@ -4988,8 +4987,8 @@ describe("Groupchats", function () {
const view = _converse.api.chatviews.get(muc_jid); const view = _converse.api.chatviews.get(muc_jid);
const textarea = await u.waitUntil(() => view.querySelector('textarea.chat-textarea')); const textarea = await u.waitUntil(() => view.querySelector('textarea.chat-textarea'));
textarea.value = 'Hello world'; textarea.value = 'Hello world';
const bottom_panel = view.querySelector('converse-muc-bottom-panel'); const message_form = view.querySelector('converse-muc-message-form');
bottom_panel.onFormSubmitted(new Event('submit')); message_form.onFormSubmitted(new Event('submit'));
await new Promise(resolve => view.model.messages.once('rendered', resolve)); await new Promise(resolve => view.model.messages.once('rendered', resolve));
let stanza = u.toStanza(` let stanza = u.toStanza(`
@ -5006,7 +5005,7 @@ describe("Groupchats", function () {
"Your message was not delivered because you weren't allowed to send it."); "Your message was not delivered because you weren't allowed to send it.");
textarea.value = 'Hello again'; textarea.value = 'Hello again';
bottom_panel.onFormSubmitted(new Event('submit')); message_form.onFormSubmitted(new Event('submit'));
await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length === 2); await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length === 2);
stanza = u.toStanza(` stanza = u.toStanza(`

View File

@ -79,7 +79,6 @@ describe("Notifications", function () {
})); }));
it("is shown for headline messages", mock.initConverse([], {}, async (done, _converse) => { it("is shown for headline messages", mock.initConverse([], {}, async (done, _converse) => {
const stub = jasmine.createSpyObj('MyNotification', ['onclick', 'close']); const stub = jasmine.createSpyObj('MyNotification', ['onclick', 'close']);
spyOn(window, 'Notification').and.returnValue(stub); spyOn(window, 'Notification').and.returnValue(stub);

View File

@ -112,8 +112,8 @@ describe("The OMEMO module", function() {
const textarea = view.querySelector('.chat-textarea'); const textarea = view.querySelector('.chat-textarea');
textarea.value = 'This message will be encrypted'; textarea.value = 'This message will be encrypted';
const bottom_panel = view.querySelector('converse-chat-bottom-panel'); const message_form = view.querySelector('converse-message-form');
bottom_panel.onKeyDown({ message_form.onKeyDown({
target: textarea, target: textarea,
preventDefault: function preventDefault () {}, preventDefault: function preventDefault () {},
keyCode: 13 // Enter keyCode: 13 // Enter
@ -294,8 +294,8 @@ describe("The OMEMO module", function() {
const textarea = view.querySelector('.chat-textarea'); const textarea = view.querySelector('.chat-textarea');
textarea.value = 'This message will be encrypted'; textarea.value = 'This message will be encrypted';
const bottom_panel = view.querySelector('converse-muc-bottom-panel'); const message_form = view.querySelector('converse-muc-message-form');
bottom_panel.onKeyDown({ message_form.onKeyDown({
target: textarea, target: textarea,
preventDefault: function preventDefault () {}, preventDefault: function preventDefault () {},
keyCode: 13 // Enter keyCode: 13 // Enter
@ -459,8 +459,8 @@ describe("The OMEMO module", function() {
const textarea = view.querySelector('.chat-textarea'); const textarea = view.querySelector('.chat-textarea');
textarea.value = 'This is an encrypted message from this device'; textarea.value = 'This is an encrypted message from this device';
const bottom_panel = view.querySelector('converse-chat-bottom-panel'); const message_form = view.querySelector('converse-message-form');
bottom_panel.onKeyDown({ message_form.onKeyDown({
target: textarea, target: textarea,
preventDefault: function preventDefault () {}, preventDefault: function preventDefault () {},
keyCode: 13 // Enter keyCode: 13 // Enter
@ -515,8 +515,8 @@ describe("The OMEMO module", function() {
const textarea = await u.waitUntil(() => view.querySelector('.chat-textarea')); const textarea = await u.waitUntil(() => view.querySelector('.chat-textarea'));
textarea.value = 'This message will be encrypted'; textarea.value = 'This message will be encrypted';
const bottom_panel = view.querySelector('converse-muc-bottom-panel'); const message_form = view.querySelector('converse-muc-message-form');
bottom_panel.onKeyDown({ message_form.onKeyDown({
target: textarea, target: textarea,
preventDefault: function preventDefault () {}, preventDefault: function preventDefault () {},
keyCode: 13 // Enter keyCode: 13 // Enter
@ -1232,8 +1232,8 @@ describe("The OMEMO module", function() {
const textarea = view.querySelector('.chat-textarea'); const textarea = view.querySelector('.chat-textarea');
textarea.value = 'This message will be sent encrypted'; textarea.value = 'This message will be sent encrypted';
const bottom_panel = view.querySelector('converse-chat-bottom-panel'); const message_form = view.querySelector('converse-message-form');
bottom_panel.onKeyDown({ message_form.onKeyDown({
target: textarea, target: textarea,
preventDefault: function preventDefault () {}, preventDefault: function preventDefault () {},
keyCode: 13 keyCode: 13

View File

@ -89,6 +89,14 @@ export default class BaseChatView extends ElementView {
} }
} }
getMessageForm () {
if (this.model.get('type') === _converse.CHATROOMS_TYPE) {
return this.querySelector('converse-muc-message-form');
} else {
return this.querySelector('converse-message-form');
}
}
/** /**
* Scrolls the chat down. * Scrolls the chat down.
* *
@ -107,7 +115,7 @@ export default class BaseChatView extends ElementView {
onWindowStateChanged (data) { onWindowStateChanged (data) {
if (data.state === 'visible') { if (data.state === 'visible') {
if (!this.model.isHidden() && this.model.get('num_unread', 0)) { if (!this.model.isHidden()) {
this.model.clearUnreadMsgCounter(); this.model.clearUnreadMsgCounter();
} }
} else if (data.state === 'hidden') { } else if (data.state === 'hidden') {

View File

@ -48,7 +48,7 @@ export default class ChatContent extends CustomElement {
render () { render () {
return html` return html`
${ this.model.ui.get('chat-content-spinner-top') ? html`<span class="spinner fa fa-spinner centered"></span>` : '' } ${ this.model.ui?.get('chat-content-spinner-top') ? html`<span class="spinner fa fa-spinner centered"></span>` : '' }
<converse-message-history <converse-message-history
.model=${this.model} .model=${this.model}
.observer=${this.observer} .observer=${this.observer}
@ -105,7 +105,7 @@ export default class ChatContent extends CustomElement {
} }
setAnchoredMessage (entries) { setAnchoredMessage (entries) {
if (this.model.ui.get('chat-content-spinner-top')) { if (!this.model?.ui || this.model.ui.get('chat-content-spinner-top')) {
return; return;
} }
entries = entries.filter(e => e.isIntersecting); entries = entries.filter(e => e.isIntersecting);

View File

@ -153,7 +153,7 @@ export default class EmojiPicker extends CustomElement {
insertIntoTextArea (value) { insertIntoTextArea (value) {
const autocompleting = this.model.get('autocompleting'); const autocompleting = this.model.get('autocompleting');
const ac_position = this.model.get('ac_position'); const ac_position = this.model.get('ac_position');
this.chatview.getBottomPanel().insertIntoTextArea(value, autocompleting, false, ac_position); this.chatview.getMessageForm().insertIntoTextArea(value, autocompleting, false, ac_position);
this.model.set({'autocompleting': null, 'query': '', 'ac_position': null}); this.model.set({'autocompleting': null, 'query': '', 'ac_position': null});
} }

View File

@ -0,0 +1,26 @@
import tpl_message_limit from './templates/message-limit.js';
import { CustomElement } from 'shared/components/element.js';
import { api } from '@converse/headless/core';
export default class MessageLimitIndicator extends CustomElement {
static get properties () {
return {
model: { type: Object }
}
}
connectedCallback () {
super.connectedCallback();
this.listenTo(this.model, 'change:draft', this.requestUpdate);
}
render () {
const limit = api.settings.get('message_limit');
if (!limit) return '';
const chars = this.model.get('draft') || '';
return tpl_message_limit(limit - chars.length);
}
}
api.elements.define('converse-message-limit-indicator', MessageLimitIndicator);

View File

@ -0,0 +1,7 @@
import { __ } from 'i18n';
import { html } from 'lit';
export default (counter) => {
const i18n_chars_remaining = __('Message characters remaining');
return html`<span class="message-limit ${counter < 1 ? 'error' : ''}" title="${i18n_chars_remaining}">${counter}</span>`;
}

View File

@ -1,7 +1,8 @@
import "./emoji-picker.js"; import './emoji-picker.js';
import 'shared/chat/message-limit.js';
import { CustomElement } from 'shared/components/element.js'; import { CustomElement } from 'shared/components/element.js';
import { __ } from 'i18n'; import { __ } from 'i18n';
import { _converse, api, converse } from "@converse/headless/core"; import { _converse, api, converse } from '@converse/headless/core';
import { html } from 'lit'; import { html } from 'lit';
import { until } from 'lit/directives/until.js'; import { until } from 'lit/directives/until.js';
@ -14,7 +15,6 @@ export class ChatToolbar extends CustomElement {
static get properties () { static get properties () {
return { return {
chatview: { type: Object }, // Used by getToolbarButtons hooks
composing_spoiler: { type: Boolean }, composing_spoiler: { type: Boolean },
hidden_occupants: { type: Boolean }, hidden_occupants: { type: Boolean },
is_groupchat: { type: Boolean }, is_groupchat: { type: Boolean },
@ -25,23 +25,38 @@ export class ChatToolbar extends CustomElement {
show_occupants_toggle: { type: Boolean }, show_occupants_toggle: { type: Boolean },
show_send_button: { type: Boolean }, show_send_button: { type: Boolean },
show_spoiler_button: { type: Boolean }, show_spoiler_button: { type: Boolean },
show_toolbar: { type: Boolean }
} }
} }
connectedCallback () {
super.connectedCallback();
this.listenTo(this.model, 'change:composing_spoiler', this.requestUpdate);
}
render () { render () {
const i18n_send_message = __('Send the message'); const i18n_send_message = __('Send the message');
return html` return html`
${ this.show_toolbar ? html`<span class="toolbar-buttons">${until(this.getButtons(), '')}</span>` : '' } <span class="toolbar-buttons">${until(this.getButtons(), '')}</span>
${ this.show_send_button ? html`<button type="submit" class="btn send-button fa fa-paper-plane" title="${ i18n_send_message }"></button>` : '' } ${ this.show_send_button ? html`<button type="submit" class="btn send-button fa fa-paper-plane" title="${ i18n_send_message }"></button>` : '' }
`; `;
} }
firstUpdated () {
/**
* Triggered once the _converse.ChatBoxView's toolbar has been rendered
* @event _converse#renderToolbar
* @type { _converse.ChatBoxView }
* @example _converse.api.listen.on('renderToolbar', this => { ... });
*/
api.trigger('renderToolbar', this);
}
getButtons () { getButtons () {
const buttons = []; const buttons = [];
if (this.show_emoji_button) { if (this.show_emoji_button) {
buttons.push(html`<converse-emoji-dropdown .chatview=${this.chatview}></converse-dropdown>`); const chatview = _converse.chatboxviews.get(this.model.get('jid'));
buttons.push(html`<converse-emoji-dropdown .chatview=${chatview}></converse-dropdown>`);
} }
if (this.show_call_button) { if (this.show_call_button) {
@ -52,10 +67,13 @@ export class ChatToolbar extends CustomElement {
</button>` </button>`
); );
} }
const i18n_chars_remaining = __('Message characters remaining');
const message_limit = api.settings.get('message_limit'); const message_limit = api.settings.get('message_limit');
if (message_limit) { if (message_limit) {
buttons.push(html`<span class="right message-limit" title="${i18n_chars_remaining}">${this.message_limit}</span>`); buttons.push(html`
<converse-message-limit-indicator .model=${this.model} class="right">
</converse-message-limit-indicator>`
);
} }
if (this.show_spoiler_button) { if (this.show_spoiler_button) {
@ -109,7 +127,7 @@ export class ChatToolbar extends CustomElement {
getSpoilerButton () { getSpoilerButton () {
const model = this.model; const model = this.model;
if (!this.is_groupchat && model.presence.resources.length === 0) { if (!this.is_groupchat && !model.presence?.resources.length) {
return; return;
} }

View File

@ -1,12 +1,11 @@
import { _converse, api } from '@converse/headless/core'; import { _converse, api } from '@converse/headless/core';
export function onScrolledDown (model) { export function onScrolledDown (model) {
if (!model.isHidden()) { if (!model.isHidden()) {
model.clearUnreadMsgCounter(); if (api.settings.get('allow_url_history_change')) {
if (api.settings.get('allow_url_history_change')) { // Clear location hash if set to one of the messages in our history
// Clear location hash if set to one of the messages in our history const hash = window.location.hash;
const hash = window.location.hash; hash && model.messages.get(hash.slice(1)) && _converse.router.history.navigate();
hash && model.messages.get(hash.slice(1)) && _converse.router.history.navigate(); }
} }
} }
}