|
@@ -1,12 +1,10 @@
|
|
|
+import BaseChatView from 'shared/chatview.js';
|
|
|
import UserDetailsModal from 'modals/user-details.js';
|
|
|
import log from '@converse/headless/log';
|
|
|
import tpl_chatbox from 'templates/chatbox.js';
|
|
|
import tpl_chatbox_head from 'templates/chatbox_head.js';
|
|
|
-import tpl_chatbox_message_form from 'templates/chatbox_message_form.js';
|
|
|
import tpl_spinner from 'templates/spinner.js';
|
|
|
-import tpl_toolbar from 'templates/toolbar.js';
|
|
|
-import { View } from '@converse/skeletor/src/view.js';
|
|
|
-import { __ } from '../../i18n';
|
|
|
+import { __ } from 'i18n';
|
|
|
import { _converse, api, converse } from '@converse/headless/core';
|
|
|
import { debounce } from 'lodash-es';
|
|
|
import { html, render } from 'lit-html';
|
|
@@ -20,12 +18,12 @@ const { dayjs } = converse.env;
|
|
|
* @namespace _converse.ChatBoxView
|
|
|
* @memberOf _converse
|
|
|
*/
|
|
|
-const ChatBoxView = View.extend({
|
|
|
- length: 200,
|
|
|
- className: 'chatbox hidden',
|
|
|
- is_chatroom: false, // Leaky abstraction from MUC
|
|
|
+export default class ChatView extends BaseChatView {
|
|
|
+ length = 200
|
|
|
+ className = 'chatbox hidden'
|
|
|
+ is_chatroom = false // Leaky abstraction from MUC
|
|
|
|
|
|
- events: {
|
|
|
+ events = {
|
|
|
'click .chatbox-navback': 'showControlBox',
|
|
|
'click .new-msgs-indicator': 'viewUnreadMessages',
|
|
|
'click .send-button': 'onFormSubmitted',
|
|
@@ -34,15 +32,20 @@ const ChatBoxView = View.extend({
|
|
|
'keydown .chat-textarea': 'onKeyDown',
|
|
|
'keyup .chat-textarea': 'onKeyUp',
|
|
|
'paste .chat-textarea': 'onPaste'
|
|
|
- },
|
|
|
+ }
|
|
|
|
|
|
async initialize () {
|
|
|
+ const jid = this.getAttribute('jid');
|
|
|
+ _converse.chatboxviews.add(jid, this);
|
|
|
+
|
|
|
+ this.model = _converse.chatboxes.get(jid);
|
|
|
this.initDebounced();
|
|
|
|
|
|
+ api.listen.on('windowStateChanged', this.onWindowStateChanged);
|
|
|
+
|
|
|
this.listenTo(this.model, 'change:composing_spoiler', this.renderMessageForm);
|
|
|
- this.listenTo(this.model, 'change:hidden', m => (m.get('hidden') ? this.hide() : this.show()));
|
|
|
+ this.listenTo(this.model, 'change:hidden', m => (!m.get('hidden') && this.show()));
|
|
|
this.listenTo(this.model, 'change:status', this.onStatusMessageChanged);
|
|
|
- this.listenTo(this.model, 'destroy', this.remove);
|
|
|
this.listenTo(this.model, 'show', this.show);
|
|
|
this.listenTo(this.model, 'vcard:change', this.renderHeading);
|
|
|
|
|
@@ -68,7 +71,6 @@ const ChatBoxView = View.extend({
|
|
|
this.listenTo(this.model, 'change:show_help_messages', this.renderHelpMessages);
|
|
|
|
|
|
await this.model.messages.fetched;
|
|
|
- this.insertIntoDOM();
|
|
|
this.model.maybeShow();
|
|
|
this.scrollDown();
|
|
|
/**
|
|
@@ -78,7 +80,7 @@ const ChatBoxView = View.extend({
|
|
|
* @example _converse.api.listen.on('chatBoxViewInitialized', view => { ... });
|
|
|
*/
|
|
|
api.trigger('chatBoxViewInitialized', this);
|
|
|
- },
|
|
|
+ }
|
|
|
|
|
|
initDebounced () {
|
|
|
this.markScrolled = debounce(this._markScrolled, 100);
|
|
@@ -93,20 +95,20 @@ const ChatBoxView = View.extend({
|
|
|
this.renderChatHistory = () => this.renderChatContent(false);
|
|
|
this.renderNotifications = () => this.renderChatContent(true);
|
|
|
}
|
|
|
- },
|
|
|
+ }
|
|
|
|
|
|
render () {
|
|
|
const result = tpl_chatbox(Object.assign(this.model.toJSON(), { 'markScrolled': ev => this.markScrolled(ev) }));
|
|
|
- render(result, this.el);
|
|
|
- this.content = this.el.querySelector('.chat-content');
|
|
|
- this.notifications = this.el.querySelector('.chat-content__notifications');
|
|
|
- this.msgs_container = this.el.querySelector('.chat-content__messages');
|
|
|
- this.help_container = this.el.querySelector('.chat-content__help');
|
|
|
+ render(result, this);
|
|
|
+ this.content = this.querySelector('.chat-content');
|
|
|
+ this.notifications = this.querySelector('.chat-content__notifications');
|
|
|
+ this.msgs_container = this.querySelector('.chat-content__messages');
|
|
|
+ this.help_container = this.querySelector('.chat-content__help');
|
|
|
this.renderChatContent();
|
|
|
this.renderMessageForm();
|
|
|
this.renderHeading();
|
|
|
return this;
|
|
|
- },
|
|
|
+ }
|
|
|
|
|
|
onMessageAdded (message) {
|
|
|
this.renderChatHistory();
|
|
@@ -122,7 +124,7 @@ const ChatBoxView = View.extend({
|
|
|
this.showNewMessagesIndicator();
|
|
|
}
|
|
|
}
|
|
|
- },
|
|
|
+ }
|
|
|
|
|
|
getNotifications () {
|
|
|
if (this.model.notifications.get('chat_state') === _converse.COMPOSING) {
|
|
@@ -134,16 +136,16 @@ const ChatBoxView = View.extend({
|
|
|
} else {
|
|
|
return '';
|
|
|
}
|
|
|
- },
|
|
|
+ }
|
|
|
|
|
|
- getHelpMessages () {
|
|
|
+ getHelpMessages () { // eslint-disable-line class-methods-use-this
|
|
|
return [
|
|
|
`<strong>/clear</strong>: ${__('Remove messages')}`,
|
|
|
`<strong>/close</strong>: ${__('Close this chat')}`,
|
|
|
`<strong>/me</strong>: ${__('Write in the third person')}`,
|
|
|
`<strong>/help</strong>: ${__('Show this menu')}`
|
|
|
];
|
|
|
- },
|
|
|
+ }
|
|
|
|
|
|
renderHelpMessages () {
|
|
|
render(
|
|
@@ -159,80 +161,22 @@ const ChatBoxView = View.extend({
|
|
|
|
|
|
this.help_container
|
|
|
);
|
|
|
- },
|
|
|
-
|
|
|
- renderChatContent (msgs_by_ref = false) {
|
|
|
- if (!this.tpl_chat_content) {
|
|
|
- this.tpl_chat_content = o => {
|
|
|
- return html`
|
|
|
- <converse-chat-content .chatview=${this} .messages=${o.messages} notifications=${o.notifications}>
|
|
|
- </converse-chat-content>
|
|
|
- `;
|
|
|
- };
|
|
|
- }
|
|
|
- const msg_models = this.model.messages.models;
|
|
|
- const messages = msgs_by_ref ? msg_models : Array.from(msg_models);
|
|
|
- render(this.tpl_chat_content({ messages, 'notifications': this.getNotifications() }), this.msgs_container);
|
|
|
- },
|
|
|
-
|
|
|
- renderToolbar () {
|
|
|
- if (!api.settings.get('show_toolbar')) {
|
|
|
- return this;
|
|
|
- }
|
|
|
- const options = Object.assign(
|
|
|
- {
|
|
|
- 'model': this.model,
|
|
|
- 'chatview': this
|
|
|
- },
|
|
|
- this.model.toJSON(),
|
|
|
- this.getToolbarOptions()
|
|
|
- );
|
|
|
- render(tpl_toolbar(options), this.el.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', view => { ... });
|
|
|
- */
|
|
|
- api.trigger('renderToolbar', this);
|
|
|
- return this;
|
|
|
- },
|
|
|
-
|
|
|
- renderMessageForm () {
|
|
|
- const form_container = this.el.querySelector('.message-form-container');
|
|
|
- render(
|
|
|
- tpl_chatbox_message_form(
|
|
|
- Object.assign(this.model.toJSON(), {
|
|
|
- 'hint_value': this.el.querySelector('.spoiler-hint')?.value,
|
|
|
- 'label_message': this.model.get('composing_spoiler') ? __('Hidden message') : __('Message'),
|
|
|
- 'label_spoiler_hint': __('Optional hint'),
|
|
|
- 'message_value': this.el.querySelector('.chat-textarea')?.value,
|
|
|
- 'show_send_button': api.settings.get('show_send_button'),
|
|
|
- 'show_toolbar': api.settings.get('show_toolbar'),
|
|
|
- 'unread_msgs': __('You have unread messages')
|
|
|
- })
|
|
|
- ),
|
|
|
- form_container
|
|
|
- );
|
|
|
- this.el.addEventListener('focusin', ev => this.emitFocused(ev));
|
|
|
- this.el.addEventListener('focusout', ev => this.emitBlurred(ev));
|
|
|
- this.renderToolbar();
|
|
|
- },
|
|
|
+ }
|
|
|
|
|
|
showControlBox () {
|
|
|
// Used in mobile view, to navigate back to the controlbox
|
|
|
_converse.chatboxviews.get('controlbox')?.show();
|
|
|
this.hide();
|
|
|
- },
|
|
|
+ }
|
|
|
|
|
|
showUserDetailsModal (ev) {
|
|
|
ev.preventDefault();
|
|
|
api.modal.show(UserDetailsModal, { model: this.model }, ev);
|
|
|
- },
|
|
|
+ }
|
|
|
|
|
|
- onDragOver (evt) {
|
|
|
+ onDragOver (evt) { // eslint-disable-line class-methods-use-this
|
|
|
evt.preventDefault();
|
|
|
- },
|
|
|
+ }
|
|
|
|
|
|
onDrop (evt) {
|
|
|
if (evt.dataTransfer.files.length == 0) {
|
|
@@ -242,33 +186,12 @@ const ChatBoxView = View.extend({
|
|
|
}
|
|
|
evt.preventDefault();
|
|
|
this.model.sendFiles(evt.dataTransfer.files);
|
|
|
- },
|
|
|
+ }
|
|
|
|
|
|
async renderHeading () {
|
|
|
const tpl = await this.generateHeadingTemplate();
|
|
|
- render(tpl, this.el.querySelector('.chat-head-chatbox'));
|
|
|
- },
|
|
|
-
|
|
|
- async getHeadingStandaloneButton (promise_or_data) {
|
|
|
- const data = await promise_or_data;
|
|
|
- return html`
|
|
|
- <a
|
|
|
- href="#"
|
|
|
- class="chatbox-btn ${data.a_class} fa ${data.icon_class}"
|
|
|
- @click=${data.handler}
|
|
|
- title="${data.i18n_title}"
|
|
|
- ></a>
|
|
|
- `;
|
|
|
- },
|
|
|
-
|
|
|
- async getHeadingDropdownItem (promise_or_data) {
|
|
|
- const data = await promise_or_data;
|
|
|
- return html`
|
|
|
- <a href="#" class="dropdown-item ${data.a_class}" @click=${data.handler} title="${data.i18n_title}"
|
|
|
- ><i class="fa ${data.icon_class}"></i>${data.i18n_text}</a
|
|
|
- >
|
|
|
- `;
|
|
|
- },
|
|
|
+ render(tpl, this.querySelector('.chat-head-chatbox'));
|
|
|
+ }
|
|
|
|
|
|
async generateHeadingTemplate () {
|
|
|
const vcard = this.model?.vcard;
|
|
@@ -295,7 +218,7 @@ const ChatBoxView = View.extend({
|
|
|
'standalone_btns': standalone_btns.map(b => this.getHeadingStandaloneButton(b))
|
|
|
})
|
|
|
);
|
|
|
- },
|
|
|
+ }
|
|
|
|
|
|
/**
|
|
|
* Returns a list of objects which represent buttons for the chat's header.
|
|
@@ -344,12 +267,12 @@ const ChatBoxView = View.extend({
|
|
|
* });
|
|
|
*/
|
|
|
return _converse.api.hook('getHeadingButtons', this, buttons);
|
|
|
- },
|
|
|
+ }
|
|
|
|
|
|
- getToolbarOptions () {
|
|
|
+ getToolbarOptions () { // eslint-disable-line class-methods-use-this
|
|
|
// FIXME: can this be removed?
|
|
|
return {};
|
|
|
- },
|
|
|
+ }
|
|
|
|
|
|
/**
|
|
|
* Scrolls the chat down, *if* appropriate.
|
|
@@ -365,32 +288,7 @@ const ChatBoxView = View.extend({
|
|
|
if ((new_own_msg || !this.model.get('scrolled')) && !this.model.isHidden()) {
|
|
|
this.debouncedScrollDown();
|
|
|
}
|
|
|
- },
|
|
|
-
|
|
|
- /**
|
|
|
- * Scrolls the chat down.
|
|
|
- *
|
|
|
- * This method will always scroll the chat down, regardless of
|
|
|
- * whether the user scrolled up manually or not.
|
|
|
- * @param { Event } [ev] - An optional event that is the cause for needing to scroll down.
|
|
|
- */
|
|
|
- scrollDown (ev) {
|
|
|
- ev?.preventDefault?.();
|
|
|
- ev?.stopPropagation?.();
|
|
|
- if (this.model.get('scrolled')) {
|
|
|
- u.safeSave(this.model, {
|
|
|
- 'scrolled': false,
|
|
|
- 'scrollTop': null
|
|
|
- });
|
|
|
- }
|
|
|
- if (this.msgs_container.scrollTo) {
|
|
|
- const behavior = this.msgs_container.scrollTop ? 'smooth' : 'auto';
|
|
|
- this.msgs_container.scrollTo({ 'top': this.msgs_container.scrollHeight, behavior });
|
|
|
- } else {
|
|
|
- this.msgs_container.scrollTop = this.msgs_container.scrollHeight;
|
|
|
- }
|
|
|
- this.onScrolledDown();
|
|
|
- },
|
|
|
+ }
|
|
|
|
|
|
/**
|
|
|
* Scroll to the previously saved scrollTop position, or scroll
|
|
@@ -403,22 +301,10 @@ const ChatBoxView = View.extend({
|
|
|
} else {
|
|
|
this.scrollDown();
|
|
|
}
|
|
|
- },
|
|
|
-
|
|
|
- insertIntoDOM () {
|
|
|
- _converse.chatboxviews.insertRowColumn(this.el);
|
|
|
- /**
|
|
|
- * Triggered once the _converse.ChatBoxView has been inserted into the DOM
|
|
|
- * @event _converse#chatBoxInsertedIntoDOM
|
|
|
- * @type { _converse.ChatBoxView | _converse.HeadlinesBoxView }
|
|
|
- * @example _converse.api.listen.on('chatBoxInsertedIntoDOM', view => { ... });
|
|
|
- */
|
|
|
- api.trigger('chatBoxInsertedIntoDOM', this);
|
|
|
- return this;
|
|
|
- },
|
|
|
+ }
|
|
|
|
|
|
addSpinner (append = false) {
|
|
|
- if (this.el.querySelector('.spinner') === null) {
|
|
|
+ if (this.querySelector('.spinner') === null) {
|
|
|
const el = u.getElementFromTemplateResult(tpl_spinner());
|
|
|
if (append) {
|
|
|
this.content.insertAdjacentElement('beforeend', el);
|
|
@@ -427,11 +313,11 @@ const ChatBoxView = View.extend({
|
|
|
this.content.insertAdjacentElement('afterbegin', el);
|
|
|
}
|
|
|
}
|
|
|
- },
|
|
|
+ }
|
|
|
|
|
|
clearSpinner () {
|
|
|
this.content.querySelectorAll('.spinner').forEach(u.removeElement);
|
|
|
- },
|
|
|
+ }
|
|
|
|
|
|
onStatusMessageChanged (item) {
|
|
|
this.renderHeading();
|
|
@@ -447,7 +333,7 @@ const ChatBoxView = View.extend({
|
|
|
'contact': item.attributes,
|
|
|
'message': item.get('status')
|
|
|
});
|
|
|
- },
|
|
|
+ }
|
|
|
|
|
|
/**
|
|
|
* Given a message element, determine wether it should be
|
|
@@ -463,7 +349,7 @@ const ChatBoxView = View.extend({
|
|
|
* @method _converse.ChatBoxView#markFollowups
|
|
|
* @param { HTMLElement } el - The message element
|
|
|
*/
|
|
|
- markFollowups (el) {
|
|
|
+ markFollowups (el) { // eslint-disable-line class-methods-use-this
|
|
|
const from = el.getAttribute('data-from');
|
|
|
const previous_el = el.previousElementSibling;
|
|
|
const date = dayjs(el.getAttribute('data-isodate'));
|
|
@@ -495,7 +381,7 @@ const ChatBoxView = View.extend({
|
|
|
} else {
|
|
|
u.removeClass('chat-msg--followup', next_el);
|
|
|
}
|
|
|
- },
|
|
|
+ }
|
|
|
|
|
|
parseMessageForCommands (text) {
|
|
|
const match = text.replace(/^\s*/, '').match(/^\/(.*)\s*$/);
|
|
@@ -511,82 +397,7 @@ const ChatBoxView = View.extend({
|
|
|
return true;
|
|
|
}
|
|
|
}
|
|
|
- },
|
|
|
-
|
|
|
- async onFormSubmitted (ev) {
|
|
|
- ev.preventDefault();
|
|
|
- const textarea = this.el.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.el.querySelector('form.sendXMPPMessage input.spoiler-hint');
|
|
|
- spoiler_hint = hint_el.value;
|
|
|
- }
|
|
|
- u.addClass('disabled', textarea);
|
|
|
- textarea.setAttribute('disabled', 'disabled');
|
|
|
- this.el.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 (message) {
|
|
|
- /**
|
|
|
- * Triggered whenever a message is sent by the user
|
|
|
- * @event _converse#messageSend
|
|
|
- * @type { _converse.Message }
|
|
|
- * @example _converse.api.listen.on('messageSend', message => { ... });
|
|
|
- */
|
|
|
- api.trigger('messageSend', message);
|
|
|
- }
|
|
|
- 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.
|
|
|
- this.msgs_container.parentElement.style.display = 'none';
|
|
|
- }
|
|
|
- textarea.removeAttribute('disabled');
|
|
|
- u.removeClass('disabled', textarea);
|
|
|
-
|
|
|
- if (api.settings.get('view_mode') === 'overlayed') {
|
|
|
- // XXX: Chrome flexbug workaround.
|
|
|
- this.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();
|
|
|
- },
|
|
|
-
|
|
|
- updateCharCounter (chars) {
|
|
|
- if (api.settings.get('message_limit')) {
|
|
|
- const message_limit = this.el.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);
|
|
|
- }
|
|
|
- }
|
|
|
- },
|
|
|
+ }
|
|
|
|
|
|
onPaste (ev) {
|
|
|
if (ev.clipboardData.files.length !== 0) {
|
|
@@ -599,28 +410,7 @@ const ChatBoxView = View.extend({
|
|
|
return;
|
|
|
}
|
|
|
this.updateCharCounter(ev.clipboardData.getData('text/plain'));
|
|
|
- },
|
|
|
-
|
|
|
- autocompleteInPicker (input, value) {
|
|
|
- const emoji_dropdown = this.el.querySelector('converse-emoji-dropdown');
|
|
|
- const emoji_picker = this.el.querySelector('converse-emoji-picker');
|
|
|
- if (emoji_picker && emoji_dropdown) {
|
|
|
- emoji_picker.model.set({
|
|
|
- 'ac_position': input.selectionStart,
|
|
|
- 'autocompleting': value,
|
|
|
- 'query': value
|
|
|
- });
|
|
|
- emoji_dropdown.showMenu();
|
|
|
- return true;
|
|
|
- }
|
|
|
- },
|
|
|
-
|
|
|
- onEmojiReceivedFromPicker (emoji) {
|
|
|
- const model = this.el.querySelector('converse-emoji-picker').model;
|
|
|
- const autocompleting = model.get('autocompleting');
|
|
|
- const ac_position = model.get('ac_position');
|
|
|
- this.insertIntoTextArea(emoji, autocompleting, false, ac_position);
|
|
|
- },
|
|
|
+ }
|
|
|
|
|
|
/**
|
|
|
* Event handler for when a depressed key goes up
|
|
@@ -629,7 +419,7 @@ const ChatBoxView = View.extend({
|
|
|
*/
|
|
|
onKeyUp (ev) {
|
|
|
this.updateCharCounter(ev.target.value);
|
|
|
- },
|
|
|
+ }
|
|
|
|
|
|
/**
|
|
|
* Event handler for when a key is pressed down in a chat box textarea.
|
|
@@ -657,14 +447,14 @@ const ChatBoxView = View.extend({
|
|
|
} else if (ev.keyCode === converse.keycodes.ENTER) {
|
|
|
return this.onEnterPressed(ev);
|
|
|
} else if (ev.keyCode === converse.keycodes.UP_ARROW && !ev.target.selectionEnd) {
|
|
|
- const textarea = this.el.querySelector('.chat-textarea');
|
|
|
+ const textarea = this.querySelector('.chat-textarea');
|
|
|
if (!textarea.value || u.hasClass('correcting', textarea)) {
|
|
|
return this.editEarlierMessage();
|
|
|
}
|
|
|
} else if (
|
|
|
ev.keyCode === converse.keycodes.DOWN_ARROW &&
|
|
|
ev.target.selectionEnd === ev.target.value.length &&
|
|
|
- u.hasClass('correcting', this.el.querySelector('.chat-textarea'))
|
|
|
+ u.hasClass('correcting', this.querySelector('.chat-textarea'))
|
|
|
) {
|
|
|
return this.editLaterMessage();
|
|
|
}
|
|
@@ -685,15 +475,11 @@ const ChatBoxView = View.extend({
|
|
|
// (which would imply an internal command and not a message).
|
|
|
this.model.setChatState(_converse.COMPOSING);
|
|
|
}
|
|
|
- },
|
|
|
+ }
|
|
|
|
|
|
getOwnMessages () {
|
|
|
return this.model.messages.filter({ 'sender': 'me' });
|
|
|
- },
|
|
|
-
|
|
|
- onEnterPressed (ev) {
|
|
|
- return this.onFormSubmitted(ev);
|
|
|
- },
|
|
|
+ }
|
|
|
|
|
|
onEscapePressed (ev) {
|
|
|
ev.preventDefault();
|
|
@@ -703,7 +489,7 @@ const ChatBoxView = View.extend({
|
|
|
message.save('correcting', false);
|
|
|
}
|
|
|
this.insertIntoTextArea('', true, false);
|
|
|
- },
|
|
|
+ }
|
|
|
|
|
|
async onMessageRetractButtonClicked (message) {
|
|
|
if (message.get('sender') !== 'me') {
|
|
@@ -723,11 +509,11 @@ const ChatBoxView = View.extend({
|
|
|
if (result) {
|
|
|
this.model.retractOwnMessage(message);
|
|
|
}
|
|
|
- },
|
|
|
+ }
|
|
|
|
|
|
onMessageEditButtonClicked (message) {
|
|
|
const currently_correcting = this.model.messages.findWhere('correcting');
|
|
|
- const unsent_text = this.el.querySelector('.chat-textarea')?.value;
|
|
|
+ const unsent_text = this.querySelector('.chat-textarea')?.value;
|
|
|
if (unsent_text && (!currently_correcting || currently_correcting.get('message') !== unsent_text)) {
|
|
|
if (!confirm(__('You have an unsent message which will be lost if you continue. Are you sure?'))) {
|
|
|
return;
|
|
@@ -742,7 +528,7 @@ const ChatBoxView = View.extend({
|
|
|
message.save('correcting', false);
|
|
|
this.insertIntoTextArea('', true, false);
|
|
|
}
|
|
|
- },
|
|
|
+ }
|
|
|
|
|
|
editLaterMessage () {
|
|
|
let message;
|
|
@@ -764,7 +550,7 @@ const ChatBoxView = View.extend({
|
|
|
} else {
|
|
|
this.insertIntoTextArea('', true, false);
|
|
|
}
|
|
|
- },
|
|
|
+ }
|
|
|
|
|
|
editEarlierMessage () {
|
|
|
let message;
|
|
@@ -789,15 +575,15 @@ const ChatBoxView = View.extend({
|
|
|
this.insertIntoTextArea(u.prefixMentions(message), true, true);
|
|
|
message.save('correcting', true);
|
|
|
}
|
|
|
- },
|
|
|
+ }
|
|
|
|
|
|
- inputChanged (ev) {
|
|
|
+ inputChanged (ev) { // eslint-disable-line class-methods-use-this
|
|
|
const height = ev.target.scrollHeight + 'px';
|
|
|
if (ev.target.style.height != height) {
|
|
|
ev.target.style.height = 'auto';
|
|
|
ev.target.style.height = height;
|
|
|
}
|
|
|
- },
|
|
|
+ }
|
|
|
|
|
|
async clearMessages (ev) {
|
|
|
if (ev && ev.preventDefault) {
|
|
@@ -808,52 +594,14 @@ const ChatBoxView = View.extend({
|
|
|
await this.model.clearMessages();
|
|
|
}
|
|
|
return this;
|
|
|
- },
|
|
|
-
|
|
|
- /**
|
|
|
- * Insert a particular string value into the textarea of this chat box.
|
|
|
- * @private
|
|
|
- * @method _converse.ChatBoxView#insertIntoTextArea
|
|
|
- * @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.el.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 + ' ';
|
|
|
- }
|
|
|
- this.updateCharCounter(textarea.value);
|
|
|
- u.placeCaretAtEnd(textarea);
|
|
|
- },
|
|
|
+ }
|
|
|
|
|
|
onPresenceChanged (item) {
|
|
|
const show = item.get('show');
|
|
|
const fullname = this.model.getDisplayName();
|
|
|
|
|
|
let text;
|
|
|
- if (u.isVisible(this.el)) {
|
|
|
+ if (u.isVisible(this)) {
|
|
|
if (show === 'offline') {
|
|
|
text = __('%1$s has gone offline', fullname);
|
|
|
} else if (show === 'away') {
|
|
@@ -865,7 +613,7 @@ const ChatBoxView = View.extend({
|
|
|
}
|
|
|
text && this.model.createMessage({ 'message': text, 'type': 'info' });
|
|
|
}
|
|
|
- },
|
|
|
+ }
|
|
|
|
|
|
async close (ev) {
|
|
|
if (ev && ev.preventDefault) {
|
|
@@ -881,7 +629,6 @@ const ChatBoxView = View.extend({
|
|
|
this.model.sendChatState();
|
|
|
}
|
|
|
await this.model.close(ev);
|
|
|
- this.remove();
|
|
|
/**
|
|
|
* Triggered once a chatbox has been closed.
|
|
|
* @event _converse#chatBoxClosed
|
|
@@ -890,154 +637,23 @@ const ChatBoxView = View.extend({
|
|
|
*/
|
|
|
api.trigger('chatBoxClosed', this);
|
|
|
return this;
|
|
|
- },
|
|
|
-
|
|
|
- emitBlurred (ev) {
|
|
|
- if (this.el.contains(document.activeElement) || this.el.contains(ev.relatedTarget)) {
|
|
|
- // Something else in this chatbox is still focused
|
|
|
- return;
|
|
|
- }
|
|
|
- /**
|
|
|
- * Triggered when the focus has been removed from a particular chat.
|
|
|
- * @event _converse#chatBoxBlurred
|
|
|
- * @type { _converse.ChatBoxView | _converse.ChatRoomView }
|
|
|
- * @example _converse.api.listen.on('chatBoxBlurred', (view, event) => { ... });
|
|
|
- */
|
|
|
- api.trigger('chatBoxBlurred', this, ev);
|
|
|
- },
|
|
|
-
|
|
|
- emitFocused (ev) {
|
|
|
- if (this.el.contains(ev.relatedTarget)) {
|
|
|
- // Something else in this chatbox was already focused
|
|
|
- return;
|
|
|
- }
|
|
|
- /**
|
|
|
- * Triggered when the focus has been moved to a particular chat.
|
|
|
- * @event _converse#chatBoxFocused
|
|
|
- * @type { _converse.ChatBoxView | _converse.ChatRoomView }
|
|
|
- * @example _converse.api.listen.on('chatBoxFocused', (view, event) => { ... });
|
|
|
- */
|
|
|
- api.trigger('chatBoxFocused', this, ev);
|
|
|
- },
|
|
|
-
|
|
|
- focus () {
|
|
|
- const textarea_el = this.el.getElementsByClassName('chat-textarea')[0];
|
|
|
- if (textarea_el && document.activeElement !== textarea_el) {
|
|
|
- textarea_el.focus();
|
|
|
- }
|
|
|
- return this;
|
|
|
- },
|
|
|
-
|
|
|
- maybeFocus () {
|
|
|
- api.settings.get('auto_focus') && this.focus();
|
|
|
- },
|
|
|
-
|
|
|
- hide () {
|
|
|
- this.el.classList.add('hidden');
|
|
|
- return this;
|
|
|
- },
|
|
|
+ }
|
|
|
|
|
|
afterShown () {
|
|
|
this.model.clearUnreadMsgCounter();
|
|
|
this.model.setChatState(_converse.ACTIVE);
|
|
|
this.scrollDown();
|
|
|
this.maybeFocus();
|
|
|
- },
|
|
|
-
|
|
|
- show () {
|
|
|
- if (this.model.get('hidden')) {
|
|
|
- log.debug(`Not showing chat ${this.model.get('jid')} because it's set as hidden`);
|
|
|
- return;
|
|
|
- }
|
|
|
- if (u.isVisible(this.el)) {
|
|
|
- this.maybeFocus();
|
|
|
- return;
|
|
|
- }
|
|
|
- if (api.settings.get('animate')) {
|
|
|
- u.fadeIn(this.el, () => this.afterShown());
|
|
|
- } else {
|
|
|
- u.showElement(this.el);
|
|
|
- this.afterShown();
|
|
|
- }
|
|
|
- },
|
|
|
+ }
|
|
|
|
|
|
showNewMessagesIndicator () {
|
|
|
- u.showElement(this.el.querySelector('.new-msgs-indicator'));
|
|
|
- },
|
|
|
-
|
|
|
- hideNewMessagesIndicator () {
|
|
|
- const new_msgs_indicator = this.el.querySelector('.new-msgs-indicator');
|
|
|
- if (new_msgs_indicator !== null) {
|
|
|
- new_msgs_indicator.classList.add('hidden');
|
|
|
- }
|
|
|
- },
|
|
|
-
|
|
|
- /**
|
|
|
- * Called when the chat content is scrolled up or down.
|
|
|
- * We want to record when the user has scrolled away from
|
|
|
- * the bottom, so that we don't automatically scroll away
|
|
|
- * from what the user is reading when new messages are received.
|
|
|
- *
|
|
|
- * Don't call this method directly, instead, call `markScrolled`,
|
|
|
- * which debounces this method by 100ms.
|
|
|
- * @private
|
|
|
- */
|
|
|
- _markScrolled: function (ev) {
|
|
|
- let scrolled = true;
|
|
|
- let scrollTop = null;
|
|
|
- const is_at_bottom =
|
|
|
- this.msgs_container.scrollTop + this.msgs_container.clientHeight >= this.msgs_container.scrollHeight - 62; // sigh...
|
|
|
-
|
|
|
- if (is_at_bottom) {
|
|
|
- scrolled = false;
|
|
|
- this.onScrolledDown();
|
|
|
- } else if (this.msgs_container.scrollTop === 0) {
|
|
|
- /**
|
|
|
- * Triggered once the chat's message area has been scrolled to the top
|
|
|
- * @event _converse#chatBoxScrolledUp
|
|
|
- * @property { _converse.ChatBoxView | _converse.ChatRoomView } view
|
|
|
- * @example _converse.api.listen.on('chatBoxScrolledUp', obj => { ... });
|
|
|
- */
|
|
|
- api.trigger('chatBoxScrolledUp', this);
|
|
|
- } else {
|
|
|
- scrollTop = ev.target.scrollTop;
|
|
|
- }
|
|
|
- u.safeSave(this.model, { scrolled, scrollTop });
|
|
|
- },
|
|
|
+ u.showElement(this.querySelector('.new-msgs-indicator'));
|
|
|
+ }
|
|
|
|
|
|
viewUnreadMessages () {
|
|
|
this.model.save({ 'scrolled': false, 'scrollTop': null });
|
|
|
this.scrollDown();
|
|
|
- },
|
|
|
-
|
|
|
- onScrolledDown () {
|
|
|
- this.hideNewMessagesIndicator();
|
|
|
- if (!this.model.isHidden()) {
|
|
|
- this.model.clearUnreadMsgCounter();
|
|
|
- // Clear location hash if set to one of the messages in our history
|
|
|
- const hash = window.location.hash;
|
|
|
- hash && this.model.messages.get(hash.slice(1)) && _converse.router.history.navigate();
|
|
|
- }
|
|
|
- /**
|
|
|
- * Triggered once the chat's message area has been scrolled down to the bottom.
|
|
|
- * @event _converse#chatBoxScrolledDown
|
|
|
- * @type {object}
|
|
|
- * @property { _converse.ChatBox | _converse.ChatRoom } chatbox - The chat model
|
|
|
- * @example _converse.api.listen.on('chatBoxScrolledDown', obj => { ... });
|
|
|
- */
|
|
|
- api.trigger('chatBoxScrolledDown', { 'chatbox': this.model }); // TODO: clean up
|
|
|
- },
|
|
|
-
|
|
|
- onWindowStateChanged (state) {
|
|
|
- if (state === 'visible') {
|
|
|
- if (!this.model.isHidden() && this.model.get('num_unread', 0)) {
|
|
|
- this.model.clearUnreadMsgCounter();
|
|
|
- }
|
|
|
- } else if (state === 'hidden') {
|
|
|
- this.model.setChatState(_converse.INACTIVE, { 'silent': true });
|
|
|
- this.model.sendChatState();
|
|
|
- }
|
|
|
}
|
|
|
-});
|
|
|
+}
|
|
|
|
|
|
-export default ChatBoxView;
|
|
|
+api.elements.define('converse-chat', ChatView);
|