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: "spec/mock.js", type: 'module' },
{ pattern: "spec/emojis.js", type: 'module' },
{ pattern: "spec/protocol.js", type: 'module' },
{ pattern: "spec/push.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/chatview/tests/chatbox.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/markers.js", type: 'module' },
{ pattern: "src/plugins/chatview/tests/me-messages.js", type: 'module' },

View File

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

View File

@ -64,6 +64,7 @@ const ChatBox = ModelWithContact.extend({
this.presence.on('change:show', item => this.onPresenceChanged(item));
}
this.on('change:chat_state', this.sendChatState, this);
this.on('change:scrolled', () => !this.get('scrolled') && this.clearUnreadMsgCounter());
await this.fetchMessages();
/**
@ -198,7 +199,7 @@ const ChatBox = ModelWithContact.extend({
* Queue an incoming `chat` message stanza for processing.
* @async
* @private
* @method _converse.ChatRoom#queueMessage
* @method _converse.ChatBox#queueMessage
* @param { Promise<MessageAttributes> } attrs - A promise which resolves to the message attributes
*/
queueMessage (attrs) {
@ -211,7 +212,7 @@ const ChatBox = ModelWithContact.extend({
/**
* @async
* @private
* @method _converse.ChatRoom#onMessage
* @method _converse.ChatBox#onMessage
* @param { MessageAttributes } attrs_promse - A promise which resolves to the message attributes.
*/
async onMessage (attrs) {
@ -681,7 +682,6 @@ const ChatBox = ModelWithContact.extend({
return _converse.connection.send(msg);
},
/**
* Finds the last eligible message and then sends a XEP-0333 chat marker for it.
* @param { ('received'|'displayed'|'acknowledged') } [type='displayed']
@ -866,7 +866,7 @@ const ChatBox = ModelWithContact.extend({
* before the collection has been fetched.
* @async
* @private
* @method _converse.ChatRoom#queueMessageCreation
* @method _converse.ChatBox#queueMessageCreation
* @param { Object } attrs
*/
async createMessage (attrs, options) {
@ -1029,6 +1029,7 @@ const ChatBox = ModelWithContact.extend({
* Given a newly received {@link _converse.Message} instance,
* update the unread counter if necessary.
* @private
* @method _converse.ChatBox#handleUnreadMessage
* @param {_converse.Message} message
*/
handleUnreadMessage (message) {
@ -1064,7 +1065,7 @@ const ChatBox = ModelWithContact.extend({
},
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:hidden', this.onHiddenChange, this);
this.on('change:scrolled', () => !this.get('scrolled') && this.clearUnreadMsgCounter());
this.on('destroy', this.removeHandlers, this);
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
* @method _converse.ChatRoom#handleUnreadMessage
* @param { XMLElement } - The <messsage> stanza
@ -2572,7 +2575,13 @@ const ChatRoomMixin = {
return;
}
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 = {
'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 tpl_toolbar from './templates/toolbar.js';
import './message-form.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 { __ } from 'i18n';
import { _converse, api, converse } from '@converse/headless/core';
import { html, render } from 'lit';
import { clearMessages, parseMessageForCommands } from './utils.js';
import { _converse, api } from '@converse/headless/core';
import { clearMessages } from './utils.js';
import { render } from 'lit';
import './styles/chat-bottom-panel.scss';
const { u } = converse.env;
export default class ChatBottomPanel extends ElementView {
events = {
'click .send-button': 'onFormSubmitted',
'click .send-button': 'sendButtonClicked',
'click .toggle-clear': 'clearMessages'
};
async connectedCallback () {
super.connectedCallback();
this.debouncedRender = debounce(this.render, 100);
this.model = _converse.chatboxes.get(this.getAttribute('jid'));
this.listenTo(this.model, 'change', o => this.onModelChanged(o.changed));
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();
}
onModelChanged (changed) {
if ('composing_spoiler' in changed || 'num_unread' in changed || 'scrolled' in changed) {
this.renderMessageForm();
}
}
render () {
render(html`<div class="message-form-container"></div>`, this);
this.renderMessageForm();
render(tpl_bottom_panel({
'model': this.model,
'viewUnreadMessages': ev => this.viewUnreadMessages(ev)
}), this);
}
renderToolbar () {
if (!api.settings.get('show_toolbar')) {
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();
sendButtonClicked (ev) {
this.querySelector('converse-message-form')?.onFormSubmitted(ev);
}
viewUnreadMessages (ev) {
@ -87,19 +44,6 @@ export default class ChatBottomPanel extends ElementView {
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) {
_converse.chatboxviews.get(this.getAttribute('jid'))?.emitFocused(ev);
}
@ -112,18 +56,6 @@ export default class ChatBottomPanel extends ElementView {
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) {
if (evt.dataTransfer.files.length == 0) {
// 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);
}
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) {
await api.emojis.initialize();
const emoji_dropdown = this.querySelector('converse-emoji-dropdown');
const emoji_picker = this.querySelector('converse-emoji-picker');
if (emoji_picker && emoji_dropdown) {
if (emoji_picker) {
emoji_picker.model.set({
'ac_position': input.selectionStart,
'autocompleting': value,
'query': value
});
emoji_dropdown.showMenu();
return true;
const emoji_dropdown = this.querySelector('converse-emoji-dropdown');
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);

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

View File

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

View File

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

View File

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

View File

@ -112,8 +112,8 @@ describe("A spoiler message", function () {
const textarea = view.querySelector('.chat-textarea');
textarea.value = 'This is the spoiler';
const bottom_panel = view.querySelector('converse-chat-bottom-panel');
bottom_panel.onKeyDown({
const message_form = view.querySelector('converse-message-form');
message_form.onKeyDown({
target: textarea,
preventDefault: function preventDefault () {},
keyCode: 13
@ -193,8 +193,8 @@ describe("A spoiler message", function () {
const hint_input = view.querySelector('.spoiler-hint');
hint_input.value = 'This is the hint';
const bottom_panel = view.querySelector('converse-chat-bottom-panel');
bottom_panel.onKeyDown({
const message_form = view.querySelector('converse-message-form');
message_form.onKeyDown({
target: textarea,
preventDefault: function preventDefault () {},
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 () {
this.model.clearUnreadMsgCounter();
this.model.setChatState(_converse.ACTIVE);
this.scrollDown();
this.maybeFocus();

View File

@ -8,7 +8,7 @@ export default (o) => {
return html`
<div class="chatbox-title ${ o.status ? '' : "chatbox-title--no-desc"}">
<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>
<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 { __ } from 'i18n';
import { _converse, api, converse } from "@converse/headless/core";
import { getAutoCompleteListItem, parseMessageForMUCCommands } from './utils.js';
import { render } from 'lit';
import './styles/muc-bottom-panel.scss';
@ -14,15 +13,15 @@ export default class MUCBottomPanel extends BottomPanel {
events = {
'click .hide-occupants': 'hideOccupants',
'click .send-button': 'onFormSubmitted',
'click .send-button': 'sendButtonClicked',
}
async connectedCallback () {
// this.model gets set in the super method and we also wait there for this.model.initialized
await super.connectedCallback();
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:num_unread_general', this.debouncedRender)
this.listenTo(this.model.features, 'change:moderated', this.debouncedRender);
this.listenTo(this.model.occupants, 'add', this.renderIfOwnOccupant)
this.listenTo(this.model.occupants, 'change:role', this.renderIfOwnOccupant);
@ -33,17 +32,21 @@ export default class MUCBottomPanel extends BottomPanel {
render () {
const entered = this.model.session.get('connection_status') === converse.ROOMSTATUS.ENTERED;
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);
if (entered && can_edit) {
this.renderMessageForm();
this.initMentionAutoComplete();
}
render(tpl_muc_bottom_panel({
can_edit, entered,
'model': this.model,
'viewUnreadMessages': ev => this.viewUnreadMessages(ev)
}), this);
}
renderIfOwnOccupant (o) {
(o.get('jid') === _converse.bare_jid) && this.debouncedRender();
}
sendButtonClicked (ev) {
this.querySelector('converse-message-form')?.onFormSubmitted(ev);
}
getToolbarOptions () {
return Object.assign(super.getToolbarOptions(), {
'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) {
ev?.preventDefault?.();
ev?.stopPropagation?.();
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);

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 () {
if (!this.model.get('hidden') && !this.model.get('minimized')) {
this.model.clearUnreadMsgCounter();
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 { api } from "@converse/headless/core";
import { html } from "lit";
import { resetElementHeight } from 'plugins/chatview/utils.js';
export default (o) => {
@ -15,16 +16,14 @@ export default (o) => {
<input type="submit" class="btn btn-primary" name="join" value="Join"/>
</form>
<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"/>
<div class="suggestion-box">
<ul class="suggestion-box__results suggestion-box__results--above" hidden=""></ul>
<textarea
autofocus
type="text"
@drop=${o.onDrop}
@input=${o.inputChanged}
@input=${resetElementHeight}
@keydown=${o.onKeyDown}
@keyup=${o.onKeyUp}
@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 { __ } from 'i18n';
import { api, converse } from "@converse/headless/core";
import { html } from "lit";
const tpl_can_edit = () => html`
<div class="emoji-picker__container dropup"></div>
<div class="message-form-container">`;
const tpl_can_edit = (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`
${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) => {
const unread_msgs = __('You have unread messages');
const conn_status = o.model.session.get('connection_status');
const i18n_not_allowed = __("You're not allowed to send messages in this room");
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) {
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>`;

View File

@ -48,10 +48,10 @@ describe("The nickname autocomplete feature", function () {
'keyCode': 50,
'key': '@'
};
const bottom_panel = view.querySelector('converse-muc-bottom-panel');
bottom_panel.onKeyDown(at_event);
const message_form = view.querySelector('converse-muc-message-form');
message_form.onKeyDown(at_event);
textarea.value = '@';
bottom_panel.onKeyUp(at_event);
message_form.onKeyUp(at_event);
await u.waitUntil(() => view.querySelectorAll('.suggestion-box__results li').length === 4);
expect(view.querySelector('.suggestion-box__results li:first-child').textContent).toBe('dick');
@ -102,11 +102,11 @@ describe("The nickname autocomplete feature", function () {
'keyCode': 50,
'key': '@'
};
const bottom_panel = view.querySelector('converse-muc-bottom-panel');
const message_form = view.querySelector('converse-muc-message-form');
textarea.value = '\n'
bottom_panel.onKeyDown(at_event);
message_form.onKeyDown(at_event);
textarea.value = '\n@';
bottom_panel.onKeyUp(at_event);
message_form.onKeyUp(at_event);
await u.waitUntil(() => view.querySelectorAll('.suggestion-box__results li').length === 4);
expect(view.querySelector('.suggestion-box__results li:first-child').textContent).toBe('dick');
@ -159,10 +159,10 @@ describe("The nickname autocomplete feature", function () {
'key': '@'
};
textarea.value = '('
const bottom_panel = view.querySelector('converse-muc-bottom-panel');
bottom_panel.onKeyDown(at_event);
const message_form = view.querySelector('converse-muc-message-form');
message_form.onKeyDown(at_event);
textarea.value = '(@';
bottom_panel.onKeyUp(at_event);
message_form.onKeyUp(at_event);
await u.waitUntil(() => view.querySelectorAll('.suggestion-box__results li').length === 4);
expect(view.querySelector('.suggestion-box__results li:first-child').textContent).toBe('dick');
@ -201,11 +201,11 @@ describe("The nickname autocomplete feature", function () {
'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
bottom_panel.onKeyDown(at_event);
message_form.onKeyDown(at_event);
textarea.value = '@ber';
bottom_panel.onKeyUp(at_event);
message_form.onKeyUp(at_event);
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: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
textarea.value = '@jo';
bottom_panel.onKeyUp(at_event);
message_form.onKeyUp(at_event);
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:nth-child(2)').textContent).toBe('jones');
@ -250,9 +250,9 @@ describe("The nickname autocomplete feature", function () {
'keyCode': 9,
'key': 'Tab'
}
const bottom_panel = view.querySelector('converse-muc-bottom-panel');
bottom_panel.onKeyDown(tab_event);
bottom_panel.onKeyUp(tab_event);
const message_form = view.querySelector('converse-muc-message-form');
message_form.onKeyDown(tab_event);
message_form.onKeyUp(tab_event);
await u.waitUntil(() => view.querySelector('.suggestion-box__results').hidden === false);
expect(view.querySelectorAll('.suggestion-box__results li').length).toBe(1);
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++) {
// 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)
bottom_panel.onKeyUp(backspace_event);
message_form.onKeyUp(backspace_event);
}
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));
textarea.value = "hello s s";
bottom_panel.onKeyDown(tab_event);
bottom_panel.onKeyUp(tab_event);
message_form.onKeyDown(tab_event);
message_form.onKeyUp(tab_event);
await u.waitUntil(() => view.querySelector('.suggestion-box__results').hidden === false);
expect(view.querySelectorAll('.suggestion-box__results li').length).toBe(2);
@ -294,13 +294,13 @@ describe("The nickname autocomplete feature", function () {
'stopPropagation': function stopPropagation () {},
'keyCode': 38
}
bottom_panel.onKeyDown(up_arrow_event);
bottom_panel.onKeyUp(up_arrow_event);
message_form.onKeyDown(up_arrow_event);
message_form.onKeyUp(up_arrow_event);
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="true"]').textContent).toBe('some2');
bottom_panel.onKeyDown({
message_form.onKeyDown({
'target': textarea,
'preventDefault': function preventDefault () {},
'stopPropagation': function stopPropagation () {},
@ -321,12 +321,12 @@ describe("The nickname autocomplete feature", function () {
});
_converse.connection._dataRecv(mock.createRequest(presence));
textarea.value = "hello z";
bottom_panel.onKeyDown(tab_event);
bottom_panel.onKeyUp(tab_event);
message_form.onKeyDown(tab_event);
message_form.onKeyUp(tab_event);
await u.waitUntil(() => view.querySelector('.suggestion-box__results').hidden === false);
bottom_panel.onKeyDown(tab_event);
bottom_panel.onKeyUp(tab_event);
message_form.onKeyDown(tab_event);
message_form.onKeyUp(tab_event);
await u.waitUntil(() => textarea.value === 'hello @z3r0 ');
done();
}));
@ -361,10 +361,10 @@ describe("The nickname autocomplete feature", function () {
'keyCode': 8,
'key': 'Backspace'
}
const bottom_panel = view.querySelector('converse-muc-bottom-panel');
bottom_panel.onKeyDown(backspace_event);
const message_form = view.querySelector('converse-muc-message-form');
message_form.onKeyDown(backspace_event);
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);
expect(view.querySelectorAll('.suggestion-box__results li').length).toBe(1);
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 textarea = await u.waitUntil(() => view.querySelector('textarea.chat-textarea'));
expect(textarea.value).toBe('');
const bottom_panel = view.querySelector('converse-muc-bottom-panel');
bottom_panel.onKeyDown({
const message_form = view.querySelector('converse-muc-message-form');
message_form.onKeyDown({
target: textarea,
keyCode: 38 // Up arrow
});
expect(textarea.value).toBe('');
textarea.value = 'But soft, what light through yonder airlock breaks?';
bottom_panel.onKeyDown({
message_form.onKeyDown({
target: textarea,
preventDefault: function preventDefault () {},
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?'});
expect(textarea.value).toBe('');
bottom_panel.onKeyDown({
message_form.onKeyDown({
target: textarea,
keyCode: 38 // Up arrow
});
@ -200,7 +200,7 @@ describe("A Groupchat Message", function () {
spyOn(_converse.connection, 'send');
const new_text = 'But soft, what light through yonder window breaks?'
textarea.value = new_text;
bottom_panel.onKeyDown({
message_form.onKeyDown({
target: textarea,
preventDefault: function preventDefault () {},
keyCode: 13 // Enter
@ -245,7 +245,7 @@ describe("A Groupchat Message", function () {
// Test that pressing the down arrow cancels message correction
expect(textarea.value).toBe('');
bottom_panel.onKeyDown({
message_form.onKeyDown({
target: textarea,
keyCode: 38 // Up arrow
});
@ -254,7 +254,7 @@ describe("A Groupchat Message", function () {
expect(view.querySelectorAll('.chat-msg').length).toBe(2);
await u.waitUntil(() => u.hasClass('correcting', view.querySelector('.chat-msg')), 500);
expect(textarea.value).toBe('But soft, what light through yonder window breaks?');
bottom_panel.onKeyDown({
message_form.onKeyDown({
target: textarea,
keyCode: 40 // Down arrow
});

View File

@ -109,8 +109,8 @@ describe("An incoming groupchat message", function () {
'stopPropagation': function stopPropagation () {},
'keyCode': 13 // Enter
}
const bottom_panel = view.querySelector('converse-muc-bottom-panel');
bottom_panel.onKeyDown(enter_event);
const message_form = view.querySelector('converse-muc-message-form');
message_form.onKeyDown(enter_event);
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 () {},
'keyCode': 13 // Enter
}
const bottom_panel = view.querySelector('converse-muc-bottom-panel');
bottom_panel.onKeyDown(enter_event);
const message_form = view.querySelector('converse-muc-message-form');
message_form.onKeyDown(enter_event);
await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length);
const sent_stanzas = _converse.connection.sent_stanzas;
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 () {},
'keyCode': 13 // Enter
}
const bottom_panel = view.querySelector('converse-muc-bottom-panel');
bottom_panel.onKeyDown(enter_event);
const message_form = view.querySelector('converse-muc-message-form');
message_form.onKeyDown(enter_event);
await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length);
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);
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 ===
'hello z3r0 gibson sw0rdf1sh, how are you?', 500);
@ -507,8 +507,8 @@ describe("A sent groupchat message", function () {
'stopPropagation': function stopPropagation () {},
'keyCode': 13 // Enter
}
const bottom_panel = view.querySelector('converse-muc-bottom-panel');
bottom_panel.onKeyDown(enter_event);
const message_form = view.querySelector('converse-muc-message-form');
message_form.onKeyDown(enter_event);
await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length);
const msg = _converse.connection.send.calls.all()[0].args[0];
@ -542,8 +542,8 @@ describe("A sent groupchat message", function () {
'stopPropagation': function stopPropagation () {},
'keyCode': 13 // Enter
}
const bottom_panel = view.querySelector('converse-muc-bottom-panel');
bottom_panel.onKeyDown(enter_event);
const message_form = view.querySelector('converse-muc-message-form');
message_form.onKeyDown(enter_event);
const message = await u.waitUntil(() => view.querySelector('.chat-msg__text'));
expect(message.innerHTML.replace(/<!-.*?->/g, '')).toEqual(
`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'));
textarea.value = '/modtools';
const enter = { 'target': textarea, 'preventDefault': function preventDefault () {}, 'keyCode': 13 };
const bottom_panel = view.querySelector('converse-muc-bottom-panel');
bottom_panel.onKeyDown(enter);
const message_form = view.querySelector('converse-muc-message-form');
message_form.onKeyDown(enter);
const modal = await u.waitUntil(() => _converse.api.modal.get('converse-modtools-modal'));
await u.waitUntil(() => u.isVisible(modal.el), 1000);
return modal;
@ -256,8 +256,8 @@ describe("The groupchat moderator tool", function () {
const textarea = await u.waitUntil(() => view.querySelector('.chat-textarea'));
textarea.value = '/modtools';
const enter = { 'target': textarea, 'preventDefault': function preventDefault () {}, 'keyCode': 13 };
const bottom_panel = view.querySelector('converse-muc-bottom-panel');
bottom_panel.onKeyDown(enter);
const message_form = view.querySelector('converse-muc-message-form');
message_form.onKeyDown(enter);
const modal = await u.waitUntil(() => _converse.api.modal.get('converse-modtools-modal'));
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'));
textarea.value = '/modtools';
const enter = { 'target': textarea, 'preventDefault': function preventDefault () {}, 'keyCode': 13 };
const bottom_panel = view.querySelector('converse-muc-bottom-panel');
bottom_panel.onKeyDown(enter);
const message_form = view.querySelector('converse-muc-message-form');
message_form.onKeyDown(enter);
const modal = await u.waitUntil(() => _converse.api.modal.get('converse-modtools-modal'));
const occupant = view.model.occupants.findWhere({'jid': _converse.bare_jid});

View File

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

View File

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

View File

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

View File

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

View File

@ -112,8 +112,8 @@ describe("The OMEMO module", function() {
const textarea = view.querySelector('.chat-textarea');
textarea.value = 'This message will be encrypted';
const bottom_panel = view.querySelector('converse-chat-bottom-panel');
bottom_panel.onKeyDown({
const message_form = view.querySelector('converse-message-form');
message_form.onKeyDown({
target: textarea,
preventDefault: function preventDefault () {},
keyCode: 13 // Enter
@ -294,8 +294,8 @@ describe("The OMEMO module", function() {
const textarea = view.querySelector('.chat-textarea');
textarea.value = 'This message will be encrypted';
const bottom_panel = view.querySelector('converse-muc-bottom-panel');
bottom_panel.onKeyDown({
const message_form = view.querySelector('converse-muc-message-form');
message_form.onKeyDown({
target: textarea,
preventDefault: function preventDefault () {},
keyCode: 13 // Enter
@ -459,8 +459,8 @@ describe("The OMEMO module", function() {
const textarea = view.querySelector('.chat-textarea');
textarea.value = 'This is an encrypted message from this device';
const bottom_panel = view.querySelector('converse-chat-bottom-panel');
bottom_panel.onKeyDown({
const message_form = view.querySelector('converse-message-form');
message_form.onKeyDown({
target: textarea,
preventDefault: function preventDefault () {},
keyCode: 13 // Enter
@ -515,8 +515,8 @@ describe("The OMEMO module", function() {
const textarea = await u.waitUntil(() => view.querySelector('.chat-textarea'));
textarea.value = 'This message will be encrypted';
const bottom_panel = view.querySelector('converse-muc-bottom-panel');
bottom_panel.onKeyDown({
const message_form = view.querySelector('converse-muc-message-form');
message_form.onKeyDown({
target: textarea,
preventDefault: function preventDefault () {},
keyCode: 13 // Enter
@ -1232,8 +1232,8 @@ describe("The OMEMO module", function() {
const textarea = view.querySelector('.chat-textarea');
textarea.value = 'This message will be sent encrypted';
const bottom_panel = view.querySelector('converse-chat-bottom-panel');
bottom_panel.onKeyDown({
const message_form = view.querySelector('converse-message-form');
message_form.onKeyDown({
target: textarea,
preventDefault: function preventDefault () {},
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.
*
@ -107,7 +115,7 @@ export default class BaseChatView extends ElementView {
onWindowStateChanged (data) {
if (data.state === 'visible') {
if (!this.model.isHidden() && this.model.get('num_unread', 0)) {
if (!this.model.isHidden()) {
this.model.clearUnreadMsgCounter();
}
} else if (data.state === 'hidden') {

View File

@ -48,7 +48,7 @@ export default class ChatContent extends CustomElement {
render () {
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
.model=${this.model}
.observer=${this.observer}
@ -105,7 +105,7 @@ export default class ChatContent extends CustomElement {
}
setAnchoredMessage (entries) {
if (this.model.ui.get('chat-content-spinner-top')) {
if (!this.model?.ui || this.model.ui.get('chat-content-spinner-top')) {
return;
}
entries = entries.filter(e => e.isIntersecting);

View File

@ -153,7 +153,7 @@ export default class EmojiPicker extends CustomElement {
insertIntoTextArea (value) {
const autocompleting = this.model.get('autocompleting');
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});
}

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 { __ } from 'i18n';
import { _converse, api, converse } from "@converse/headless/core";
import { _converse, api, converse } from '@converse/headless/core';
import { html } from 'lit';
import { until } from 'lit/directives/until.js';
@ -14,7 +15,6 @@ export class ChatToolbar extends CustomElement {
static get properties () {
return {
chatview: { type: Object }, // Used by getToolbarButtons hooks
composing_spoiler: { type: Boolean },
hidden_occupants: { type: Boolean },
is_groupchat: { type: Boolean },
@ -25,23 +25,38 @@ export class ChatToolbar extends CustomElement {
show_occupants_toggle: { type: Boolean },
show_send_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 () {
const i18n_send_message = __('Send the message');
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>` : '' }
`;
}
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 () {
const buttons = [];
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) {
@ -52,10 +67,13 @@ export class ChatToolbar extends CustomElement {
</button>`
);
}
const i18n_chars_remaining = __('Message characters remaining');
const message_limit = api.settings.get('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) {
@ -109,7 +127,7 @@ export class ChatToolbar extends CustomElement {
getSpoilerButton () {
const model = this.model;
if (!this.is_groupchat && model.presence.resources.length === 0) {
if (!this.is_groupchat && !model.presence?.resources.length) {
return;
}

View File

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