import tplMessageForm from './templates/message-form.js';
import { ElementView } from '@converse/skeletor/src/element.js';
import { __ } from 'i18n';
import { _converse, api, converse } from "@converse/headless/core.js";
import { parseMessageForCommands } from './utils.js';
import { prefixMentions } from '@converse/headless/utils/core.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.listenTo(this.model, 'change:composing_spoiler', () => this.render());
this.handleEmojiSelection = ({ detail }) => {
if (this.model.get('jid') === detail.jid) {
this.insertIntoTextArea(detail.value, detail.autocompleting, false, detail.ac_position);
}
}
document.addEventListener("emojiSelected", this.handleEmojiSelection);
this.render();
}
disconnectedCallback () {
super.disconnectedCallback();
document.removeEventListener("emojiSelected", this.handleEmojiSelection);
}
toHTML () {
return tplMessageForm(
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 { number } [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(prefixMentions(message), true, true);
} else {
const currently_correcting = this.model.messages.findWhere('correcting');
if (currently_correcting && currently_correcting !== message) {
this.insertIntoTextArea(prefixMentions(message), true, true);
} else {
this.insertIntoTextArea('', true, false);
}
}
}
onEscapePressed (ev) {
const idx = this.model.messages.findLastIndex('correcting');
const message = idx >= 0 ? this.model.messages.at(idx) : null;
if (message) {
ev.preventDefault();
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);
}
}
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 = await parseMessageForCommands(this.model, message_text);
const message = is_command ? null : await this.model.sendMessage({'body': 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);