Commit 2b6c56f1 authored by JC Brand's avatar JC Brand

Move converse-chatview plugin into folder

parent ecfaba07
......@@ -16,7 +16,7 @@ import "shared/registry.js";
*/
import "./plugins/autocomplete.js";
import "./plugins/bookmark-views.js"; // Views for XEP-0048 Bookmarks
import "./plugins/chatview.js"; // Renders standalone chat boxes for single user chat
import "./plugins/chatview/index.js"; // Renders standalone chat boxes for single user chat
import "./plugins/controlbox/index.js"; // The control box
import "./plugins/dragresize.js"; // Allows chat boxes to be resized by dragging them
import "./plugins/fullscreen.js";
......
import { _converse } from '@converse/headless/core';
export default {
/**
* The "chatview" namespace groups methods pertaining to views
* for one-on-one chats.
*
* @namespace _converse.api.chatviews
* @memberOf _converse.api
*/
chatviews: {
/**
* Get the view of an already open chat.
* @method _converse.api.chatviews.get
* @param { Array.string | string } jids
* @returns { _converse.ChatBoxView|undefined } The chat should already be open, otherwise `undefined` will be returned.
* @example
* // To return a single view, provide the JID of the contact:
* _converse.api.chatviews.get('buddy@example.com')
* @example
* // To return an array of views, provide an array of JIDs:
* _converse.api.chatviews.get(['buddy1@example.com', 'buddy2@example.com'])
*/
get (jids) {
if (jids === undefined) {
return Object.values(_converse.chatboxviews.getAll());
}
if (typeof jids === 'string') {
return _converse.chatboxviews.get(jids);
}
return jids.map(jid => _converse.chatboxviews.get(jid));
}
}
}
/**
* @module converse-chatview
* @copyright 2020, the Converse.js contributors
* @license Mozilla Public License (MPLv2)
*/
import '../../components/chat_content.js';
import '../../components/help_messages.js';
import '../../components/toolbar.js';
import '../chatboxviews/index.js';
import '../modal.js';
import { _converse, api, converse } from '@converse/headless/core';
import ChatBoxView from './view.js';
import chatview_api from './api.js';
const { Strophe } = converse.env;
function onWindowStateChanged (data) {
if (_converse.chatboxviews) {
_converse.chatboxviews.forEach(view => {
if (view.model.get('id') !== 'controlbox') {
view.onWindowStateChanged(data.state);
}
});
}
}
function onChatBoxViewsInitialized () {
const views = _converse.chatboxviews;
_converse.chatboxes.on('add', async item => {
if (!views.get(item.get('id')) && item.get('type') === _converse.PRIVATE_CHAT_TYPE) {
await item.initialized;
views.add(item.get('id'), new _converse.ChatBoxView({ model: item }));
}
});
}
converse.plugins.add('converse-chatview', {
/* Plugin dependencies are other plugins which might be
* overridden or relied upon, and therefore need to be loaded before
* this plugin.
*
* If the setting "strict_plugin_dependencies" is set to true,
* an error will be raised if the plugin is not found. By default it's
* false, which means these plugins are only loaded opportunistically.
*
* NB: These plugins need to have already been loaded via require.js.
*/
dependencies: ['converse-chatboxviews', 'converse-chat', 'converse-disco', 'converse-modal'],
initialize () {
/* The initialize function gets called as soon as the plugin is
* loaded by converse.js's plugin machinery.
*/
api.settings.extend({
'auto_focus': true,
'debounced_content_rendering': true,
'filter_url_query_params': null,
'image_urls_regex': null,
'message_limit': 0,
'muc_hats': ['xep317'],
'show_images_inline': true,
'show_message_avatar': true,
'show_retraction_warning': true,
'show_send_button': true,
'show_toolbar': true,
'time_format': 'HH:mm',
'use_system_emojis': true,
'visible_toolbar_buttons': {
'call': false,
'clear': true,
'emoji': true,
'spoiler': true
}
});
Object.assign(api, chatview_api);
_converse.ChatBoxView = ChatBoxView;
api.listen.on('chatBoxViewsInitialized', onChatBoxViewsInitialized);
api.listen.on('windowStateChanged', onWindowStateChanged);
api.listen.on('connected', () => api.disco.own.features.add(Strophe.NS.SPOILER));
}
});
/**
* @module converse-chatview
* @copyright 2020, the Converse.js contributors
* @license Mozilla Public License (MPLv2)
*/
import '../components/chat_content.js';
import '../components/help_messages.js';
import '../components/toolbar.js';
import './chatboxviews/index.js';
import './modal.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 UserDetailsModal from 'modals/user-details.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';
const { Strophe, dayjs } = converse.env;
const u = converse.env.utils;
const { dayjs } = converse.env;
/**
* The View of an open/ongoing chat conversation.
......@@ -32,10 +20,10 @@ const u = converse.env.utils;
* @namespace _converse.ChatBoxView
* @memberOf _converse
*/
export const ChatBoxView = View.extend({
const ChatBoxView = View.extend({
length: 200,
className: 'chatbox hidden',
is_chatroom: false, // Leaky abstraction from MUC
is_chatroom: false, // Leaky abstraction from MUC
events: {
'click .chatbox-navback': 'showControlBox',
......@@ -45,14 +33,14 @@ export const ChatBoxView = View.extend({
'input .chat-textarea': 'inputChanged',
'keydown .chat-textarea': 'onKeyDown',
'keyup .chat-textarea': 'onKeyUp',
'paste .chat-textarea': 'onPaste',
'paste .chat-textarea': 'onPaste'
},
async initialize () {
this.initDebounced();
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.hide() : this.show()));
this.listenTo(this.model, 'change:status', this.onStatusMessageChanged);
this.listenTo(this.model, 'destroy', this.remove);
this.listenTo(this.model, 'show', this.show);
......@@ -108,9 +96,7 @@ export const ChatBoxView = View.extend({
},
render () {
const result = tpl_chatbox(
Object.assign(this.model.toJSON(), {'markScrolled': ev => this.markScrolled(ev)})
);
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');
......@@ -161,41 +147,40 @@ export const ChatBoxView = View.extend({
renderHelpMessages () {
render(
html`<converse-chat-help
.model=${this.model}
.messages=${this.getHelpMessages()}
?hidden=${!this.model.get('show_help_messages')}
type="info"
chat_type="${this.model.get('type')}"></converse-chat-help>`,
html`
<converse-chat-help
.model=${this.model}
.messages=${this.getHelpMessages()}
?hidden=${!this.model.get('show_help_messages')}
type="info"
chat_type="${this.model.get('type')}"
></converse-chat-help>
`,
this.help_container
);
},
renderChatContent (msgs_by_ref=false) {
renderChatContent (msgs_by_ref = false) {
if (!this.tpl_chat_content) {
this.tpl_chat_content = (o) => {
this.tpl_chat_content = o => {
return html`
<converse-chat-content
.chatview=${this}
.messages=${o.messages}
notifications=${o.notifications}>
</converse-chat-content>`
<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
);
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({
const options = Object.assign(
{
'model': this.model,
'chatview': this
},
......@@ -215,16 +200,20 @@ export const ChatBoxView = View.extend({
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);
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();
......@@ -238,7 +227,7 @@ export const ChatBoxView = View.extend({
showUserDetailsModal (ev) {
ev.preventDefault();
api.modal.show(UserDetailsModal, {model: this.model}, ev);
api.modal.show(UserDetailsModal, { model: this.model }, ev);
},
onDragOver (evt) {
......@@ -262,43 +251,49 @@ export const ChatBoxView = View.extend({
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>`;
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>`;
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
>
`;
},
async generateHeadingTemplate () {
const vcard = this.model?.vcard;
const vcard_json = vcard ? vcard.toJSON() : {};
const i18n_profile = __('The User\'s Profile Image');
const avatar_data = Object.assign({
'alt_text': i18n_profile,
'extra_classes': '',
'height': 40,
'width': 40,
}, vcard_json);
const i18n_profile = __("The User's Profile Image");
const avatar_data = Object.assign(
{
'alt_text': i18n_profile,
'extra_classes': '',
'height': 40,
'width': 40
},
vcard_json
);
const heading_btns = await this.getHeadingButtons();
const standalone_btns = heading_btns.filter(b => b.standalone);
const dropdown_btns = heading_btns.filter(b => !b.standalone);
return tpl_chatbox_head(
Object.assign(
this.model.toJSON(), {
avatar_data,
'display_name': this.model.getDisplayName(),
'dropdown_btns': dropdown_btns.map(b => this.getHeadingDropdownItem(b)),
'showUserDetailsModal': ev => this.showUserDetailsModal(ev),
'standalone_btns': standalone_btns.map(b => this.getHeadingStandaloneButton(b)),
}
)
Object.assign(this.model.toJSON(), {
avatar_data,
'display_name': this.model.getDisplayName(),
'dropdown_btns': dropdown_btns.map(b => this.getHeadingDropdownItem(b)),
'showUserDetailsModal': ev => this.showUserDetailsModal(ev),
'standalone_btns': standalone_btns.map(b => this.getHeadingStandaloneButton(b))
})
);
},
......@@ -310,16 +305,18 @@ export const ChatBoxView = View.extend({
* @method _converse.ChatBoxView#getHeadingButtons
*/
getHeadingButtons () {
const buttons = [{
'a_class': 'show-user-details-modal',
'handler': ev => this.showUserDetailsModal(ev),
'i18n_text': __('Details'),
'i18n_title': __('See more information about this person'),
'icon_class': 'fa-id-card',
'name': 'details',
'standalone': api.settings.get("view_mode") === 'overlayed',
}];
if (!api.settings.get("singleton")) {
const buttons = [
{
'a_class': 'show-user-details-modal',
'handler': ev => this.showUserDetailsModal(ev),
'i18n_text': __('Details'),
'i18n_title': __('See more information about this person'),
'icon_class': 'fa-id-card',
'name': 'details',
'standalone': api.settings.get('view_mode') === 'overlayed'
}
];
if (!api.settings.get('singleton')) {
buttons.push({
'a_class': 'close-chatbox-button',
'handler': ev => this.close(ev),
......@@ -327,7 +324,7 @@ export const ChatBoxView = View.extend({
'i18n_title': __('Close and end this conversation'),
'icon_class': 'fa-times',
'name': 'close',
'standalone': api.settings.get("view_mode") === 'overlayed',
'standalone': api.settings.get('view_mode') === 'overlayed'
});
}
/**
......@@ -364,7 +361,7 @@ export const ChatBoxView = View.extend({
* - An optional message that serves as the cause for needing to scroll down.
*/
maybeScrollDown (message) {
const new_own_msg = !(message?.get('is_archived')) && message?.get('sender') === 'me';
const new_own_msg = !message?.get('is_archived') && message?.get('sender') === 'me';
if ((new_own_msg || !this.model.get('scrolled')) && !this.model.isHidden()) {
this.debouncedScrollDown();
}
......@@ -383,12 +380,12 @@ export const ChatBoxView = View.extend({
if (this.model.get('scrolled')) {
u.safeSave(this.model, {
'scrolled': false,
'scrollTop': null,
'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});
this.msgs_container.scrollTo({ 'top': this.msgs_container.scrollHeight, behavior });
} else {
this.msgs_container.scrollTop = this.msgs_container.scrollHeight;
}
......@@ -420,7 +417,7 @@ export const ChatBoxView = View.extend({
return this;
},
addSpinner (append=false) {
addSpinner (append = false) {
if (this.el.querySelector('.spinner') === null) {
const el = u.getElementFromTemplateResult(tpl_spinner());
if (append) {
......@@ -472,19 +469,28 @@ export const ChatBoxView = View.extend({
const date = dayjs(el.getAttribute('data-isodate'));
const next_el = el.nextElementSibling;
if (!u.hasClass('chat-msg--action', el) && !u.hasClass('chat-msg--action', previous_el) &&
!u.hasClass('chat-info', el) && !u.hasClass('chat-info', previous_el) &&
previous_el.getAttribute('data-from') === from &&
date.isBefore(dayjs(previous_el.getAttribute('data-isodate')).add(10, 'minutes')) &&
el.getAttribute('data-encrypted') === previous_el.getAttribute('data-encrypted')) {
if (
!u.hasClass('chat-msg--action', el) &&
!u.hasClass('chat-msg--action', previous_el) &&
!u.hasClass('chat-info', el) &&
!u.hasClass('chat-info', previous_el) &&
previous_el.getAttribute('data-from') === from &&
date.isBefore(dayjs(previous_el.getAttribute('data-isodate')).add(10, 'minutes')) &&
el.getAttribute('data-encrypted') === previous_el.getAttribute('data-encrypted')
) {
u.addClass('chat-msg--followup', el);
}
if (!next_el) { return; }
if (!next_el) {
return;
}
if (!u.hasClass('chat-msg--action', el) && u.hasClass('chat-info', el) &&
next_el.getAttribute('data-from') === from &&
dayjs(next_el.getAttribute('data-isodate')).isBefore(date.add(10, 'minutes')) &&
el.getAttribute('data-encrypted') === next_el.getAttribute('data-encrypted')) {
if (
!u.hasClass('chat-msg--action', el) &&
u.hasClass('chat-info', el) &&
next_el.getAttribute('data-from') === from &&
dayjs(next_el.getAttribute('data-isodate')).isBefore(date.add(10, 'minutes')) &&
el.getAttribute('data-encrypted') === next_el.getAttribute('data-encrypted')
) {
u.addClass('chat-msg--followup', next_el);
} else {
u.removeClass('chat-msg--followup', next_el);
......@@ -492,16 +498,16 @@ export const ChatBoxView = View.extend({
},
parseMessageForCommands (text) {
const match = text.replace(/^\s*/, "").match(/^\/(.*)\s*$/);
const match = text.replace(/^\s*/, '').match(/^\/(.*)\s*$/);
if (match) {
if (match[1] === "clear") {
if (match[1] === 'clear') {
this.clearMessages();
return true;
} else if (match[1] === "close") {
} else if (match[1] === 'close') {
this.close();
return true;
} else if (match[1] === "help") {
this.model.set({'show_help_messages': true});
} else if (match[1] === 'help') {
this.model.set({ 'show_help_messages': true });
return true;
}
}
......@@ -511,8 +517,10 @@ export const ChatBoxView = View.extend({
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) {
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) {
......@@ -521,7 +529,8 @@ export const ChatBoxView = View.extend({
api.connection.reconnect();
return;
}
let spoiler_hint, hint_el = {};
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;
......@@ -548,7 +557,7 @@ export const ChatBoxView = View.extend({
*/
api.trigger('messageSend', message);
}
if (api.settings.get("view_mode") === 'overlayed') {
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';
......@@ -556,13 +565,13 @@ export const ChatBoxView = View.extend({
textarea.removeAttribute('disabled');
u.removeClass('disabled', textarea);
if (api.settings.get("view_mode") === 'overlayed') {
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});
this.model.setChatState(_converse.ACTIVE, { 'silent': true });
textarea.focus();
},
......@@ -652,17 +661,23 @@ export const ChatBoxView = View.extend({
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'))) {
} else if (
ev.keyCode === converse.keycodes.DOWN_ARROW &&
ev.target.selectionEnd === ev.target.value.length &&
u.hasClass('correcting', this.el.querySelector('.chat-textarea'))
) {
return this.editLaterMessage();
}
}
if ([converse.keycodes.SHIFT,
if (
[
converse.keycodes.SHIFT,
converse.keycodes.META,
converse.keycodes.META_RIGHT,
converse.keycodes.ESCAPE,
converse.keycodes.ALT].includes(ev.keyCode)) {
converse.keycodes.ALT
].includes(ev.keyCode)
) {
return;
}
if (this.model.get('chat_state') !== _converse.COMPOSING) {
......@@ -673,7 +688,7 @@ export const ChatBoxView = View.extend({
},
getOwnMessages () {
return this.model.messages.filter({'sender': 'me'});
return this.model.messages.filter({ 'sender': 'me' });
},
onEnterPressed (ev) {
......@@ -683,7 +698,7 @@ export const ChatBoxView = View.extend({
onEscapePressed (ev) {
ev.preventDefault();
const idx = this.model.messages.findLastIndex('correcting');
const message = idx >=0 ? this.model.messages.at(idx) : null;
const message = idx >= 0 ? this.model.messages.at(idx) : null;
if (message) {
message.save('correcting', false);
}
......@@ -694,10 +709,11 @@ export const ChatBoxView = View.extend({
if (message.get('sender') !== 'me') {
return log.error("onMessageRetractButtonClicked called for someone else's message!");
}
const retraction_warning =
__("Be aware that other XMPP/Jabber clients (and servers) may "+
"not yet support retractions and that this message may not "+
"be removed everywhere.");
const retraction_warning = __(
'Be aware that other XMPP/Jabber clients (and servers) may ' +
'not yet support retractions and that this message may not ' +
'be removed everywhere.'
);
const messages = [__('Are you sure you want to retract this message?')];
if (api.settings.get('show_retraction_warning')) {
......@@ -713,7 +729,7 @@ export const ChatBoxView = View.extend({
const currently_correcting = this.model.messages.findWhere('correcting');
const unsent_text = this.el.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?"))) {
if (!confirm(__('You have an unsent message which will be lost if you continue. Are you sure?'))) {
return;
}
}
......@@ -733,7 +749,7 @@ export const ChatBoxView = View.extend({
let idx = this.model.messages.findLastIndex('correcting');
if (idx >= 0) {
this.model.messages.at(idx).save('correcting', false);
while (idx < this.model.messages.length-1) {
while (idx < this.model.messages.length - 1) {
idx += 1;
const candidate = this.model.messages.at(idx);
if (candidate.get('editable')) {
......@@ -764,7 +780,11 @@ export const ChatBoxView = View.extend({
}
}
}
message = message || this.getOwnMessages().reverse().find(m => m.get('editable'));
message =
message ||
this.getOwnMessages()
.reverse()
.find(m => m.get('editable'));
if (message) {
this.insertIntoTextArea(u.prefixMentions(message), true, true);
message.save('correcting', true);
......@@ -780,8 +800,10 @@ export const ChatBoxView = View.extend({
},
async clearMessages (ev) {
if (ev && ev.preventDefault) { ev.preventDefault(); }
const result = confirm(__("Are you sure you want to clear the messages from this conversation?"));
if (ev && ev.preventDefault) {
ev.preventDefault();
}
const result = confirm(__('Are you sure you want to clear the messages from this conversation?'));
if (result === true) {
await this.model.clearMessages();
}
......@@ -800,7 +822,7 @@ export const ChatBoxView = View.extend({
* @param {integer} [position] - The end index of the string to be
* replaced with the new value.
*/
insertIntoTextArea (value, replace=false, correcting=false, position) {
insertIntoTextArea (value, replace = false, correcting = false, position) {
const textarea = this.el.querySelector('.chat-textarea');
if (correcting) {
u.addClass('correcting', textarea);
......@@ -809,19 +831,18 @@ export const ChatBoxView = View.extend({
}
if (replace) {
if (position && typeof replace == 'string') {
textarea.value = textarea.value.replace(
new RegExp(replace, 'g'),
(match, offset) => (offset == position-replace.length ? value+' ' : match)
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] !== ' ')) {
if (existing && existing[existing.length - 1] !== ' ') {
existing = existing + ' ';
}
textarea.value = existing+value+' ';
textarea.value = existing + value + ' ';
}
this.updateCharCounter(textarea.value);
u.placeCaretAtEnd(textarea);
......@@ -837,18 +858,20 @@ export const ChatBoxView = View.extend({
text = __('%1$s has gone offline', fullname);
} else if (show === 'away') {
text = __('%1$s has gone away', fullname);
} else if ((show === 'dnd')) {
} else if (show === 'dnd') {
text = __('%1$s is busy', fullname);
} else if (show === 'online') {
text = __('%1$s is online', fullname);
}
text && this.model.createMessage({'message': text, 'type': 'info'});
text && this.model.createMessage({ 'message': text, 'type': 'info' });
}
},
async close (ev) {
if (ev && ev.preventDefault) { ev.preventDefault(); }
if (_converse.router.history.getFragment() === "converse/chat?jid="+this.model.get('jid')) {
if (ev && ev.preventDefault) {
ev.preventDefault();
}
if (_converse.router.history.getFragment() === 'converse/chat?jid=' + this.model.get('jid')) {
_converse.router.navigate('');
}
if (api.connection.connected()) {
......@@ -963,8 +986,7 @@ export const ChatBoxView = View.extend({
let scrolled = true;
let scrollTop = null;
const is_at_bottom =
(this.msgs_container.scrollTop + this.msgs_container.clientHeight) >=
this.msgs_container.scrollHeight - 62; // sigh...
this.msgs_container.scrollTop + this.msgs_container.clientHeight >= this.msgs_container.scrollHeight - 62; // sigh...
if (is_at_bottom) {
scrolled = false;
......@@ -984,7 +1006,7 @@ export const ChatBoxView = View.extend({
},
viewUnreadMessages () {
this.model.save({'scrolled': false, 'scrollTop': null});
this.model.save({ 'scrolled': false, 'scrollTop': null });
this.scrollDown();
},
......@@ -1003,7 +1025,7 @@ export const ChatBoxView = View.extend({
* @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
api.trigger('chatBoxScrolledDown', { 'chatbox': this.model }); // TODO: clean up
},
onWindowStateChanged (state) {
......@@ -1015,118 +1037,10 @@ export const ChatBoxView = View.extend({
}
}
} else if (state === 'hidden') {
this.model.setChatState(_converse.INACTIVE, {'silent': true});
this.model.setChatState(_converse.INACTIVE, { 'silent': true });
this.model.sendChatState();
}
}
});
converse.plugins.add('converse-chatview', {
/* Plugin dependencies are other plugins which might be
* overridden or relied upon, and therefore need to be loaded before
* this plugin.
*
* If the setting "strict_plugin_dependencies" is set to true,
* an error will be raised if the plugin is not found. By default it's
* false, which means these plugins are only loaded opportunistically.
*
* NB: These plugins need to have already been loaded via require.js.
*/
dependencies: [
"converse-chatboxviews",
"converse-chat",
"converse-disco",
"converse-modal"
],
initialize () {
/* The initialize function gets called as soon as the plugin is
* loaded by converse.js's plugin machinery.
*/
api.settings.extend({
'auto_focus': true,
'debounced_content_rendering': true,
'filter_url_query_params': null,
'image_urls_regex': null,
'message_limit': 0,
'muc_hats': ['xep317'],
'show_images_inline': true,
'show_message_avatar': true,
'show_retraction_warning': true,
'show_send_button': true,
'show_toolbar': true,
'time_format': 'HH:mm',
'use_system_emojis': true,
'visible_toolbar_buttons': {
'call': false,
'clear': true,
'emoji': true,
'spoiler': true
},
});
_converse.ChatBoxView = ChatBoxView;
api.listen.on('chatBoxViewsInitialized', () => {
const views = _converse.chatboxviews;
_converse.chatboxes.on('add', async item => {
if (!views.get(item.get('id')) && item.get('type') === _converse.PRIVATE_CHAT_TYPE) {
await item.initialized;
views.add(item.get('id'), new _converse.ChatBoxView({model: item}));
}
});
});
/************************ BEGIN Event Handlers ************************/
function onWindowStateChanged (data) {
if (_converse.chatboxviews) {
_converse.chatboxviews.forEach(view => {
if (view.model.get('id') !== 'controlbox') {
view.onWindowStateChanged(data.state);
}
});
}
}
api.listen.on('windowStateChanged', onWindowStateChanged);
api.listen.on('connected', () => api.disco.own.features.add(Strophe.NS.SPOILER));
/************************ END Event Handlers ************************/
/************************ BEGIN API ************************/
Object.assign(api, {
/**
* The "chatview" namespace groups methods pertaining to views
* for one-on-one chats.
*
* @namespace _converse.api.chatviews
* @memberOf _converse.api
*/
chatviews: {
/**
* Get the view of an already open chat.
* @method _converse.api.chatviews.get
* @param { Array.string | string } jids
* @returns { _converse.ChatBoxView|undefined } The chat should already be open, otherwise `undefined` will be returned.
* @example
* // To return a single view, provide the JID of the contact:
* _converse.api.chatviews.get('buddy@example.com')
* @example
* // To return an array of views, provide an array of JIDs:
* _converse.api.chatviews.get(['buddy1@example.com', 'buddy2@example.com'])
*/
get (jids) {
if (jids === undefined) {
return Object.values(_converse.chatboxviews.getAll());
}
if (typeof jids === 'string') {
return _converse.chatboxviews.get(jids);
}
return jids.map(jid => _converse.chatboxviews.get(jid));
}
}
});
/************************ END API ************************/
}
});
export default ChatBoxView;
......@@ -4,7 +4,7 @@
* @license Mozilla Public License (MPLv2)
*/
import "../../components/brand-heading";
import "../chatview";
import "../chatview/index.js";
import ControlBoxMixin from './model.js';
import ControlBoxPane from './pane.js';
import ControlBoxToggle from './toggle.js';
......
......@@ -3,7 +3,7 @@
* @copyright 2020, the Converse.js contributors
* @license Mozilla Public License (MPLv2)
*/
import "./chatview.js";
import "./chatview/index.js";
import "./controlbox/index.js";
import { debounce } from "lodash-es";
import { _converse, api, converse } from "@converse/headless/core";
......
......@@ -3,7 +3,7 @@
* @license Mozilla Public License (MPLv2)
* @copyright 2020, the Converse.js contributors
*/
import "./chatview.js";
import "./chatview/index.js";
import "./controlbox/index.js";
import "./singleton.js";
import "@converse/headless/plugins/muc";
......
......@@ -3,9 +3,9 @@
* @copyright 2020, the Converse.js contributors
* @license Mozilla Public License (MPLv2)
*/
import "./chatview/index.js";
import tpl_chatbox from "../templates/chatbox.js";
import tpl_headline_panel from "../templates/headline_panel.js";
import { ChatBoxView } from "./chatview";
import { View } from '@converse/skeletor/src/view.js';
import { __ } from '../i18n';
import { _converse, api, converse } from "@converse/headless/core";
......@@ -14,7 +14,7 @@ import { render } from "lit-html";
const u = converse.env.utils;
const HeadlinesBoxView = ChatBoxView.extend({
const HeadlinesBoxViewMixin = {
className: 'chatbox headlines hidden',
events: {
......@@ -100,10 +100,10 @@ const HeadlinesBoxView = ChatBoxView.extend({
return _converse.api.hook('getHeadingButtons', this, buttons);
},
// Override to avoid the methods in converse-chatview.js
// Override to avoid the methods in converse-chatview
'renderMessageForm': function renderMessageForm () {},
'afterShown': function afterShown () {}
});
};
/**
......@@ -210,7 +210,7 @@ converse.plugins.add('converse-headlines-view', {
Object.assign(_converse.ControlBoxView.prototype, viewWithHeadlinesPanel);
}
_converse.HeadlinesBoxView = HeadlinesBoxView;
_converse.HeadlinesBoxView = _converse.ChatBoxView.extend(HeadlinesBoxViewMixin);
_converse.HeadlinesPanel = HeadlinesPanel;
......
......@@ -4,7 +4,7 @@
* @license Mozilla Public License (MPLv2)
*/
import '../components/minimized_chat.js';
import './chatview.js';
import './chatview/index.js';
import tpl_chats_panel from '../templates/chats_panel.js';
import { Model } from '@converse/skeletor/src/model.js';
import { View } from '@converse/skeletor/src/view';
......
......@@ -5,6 +5,7 @@
* @license Mozilla Public License (MPLv2)
*/
import "../components/muc-sidebar";
import "./chatview/index.js";
import "./modal.js";
import "@converse/headless/utils/muc";
import AddMUCModal from '../modals/add-muc.js';
......@@ -24,7 +25,6 @@ import tpl_muc_nickname_form from "../templates/muc_nickname_form.js";
import tpl_muc_password_form from "../templates/muc_password_form.js";
import tpl_room_panel from "../templates/room_panel.js";
import tpl_spinner from "../templates/spinner.js";
import { ChatBoxView } from "./chatview.js";
import { Model } from '@converse/skeletor/src/model.js';
import { View } from '@converse/skeletor/src/view.js';
import { __ } from '../i18n';
......@@ -63,7 +63,7 @@ const COMMAND_TO_AFFILIATION = {
* @namespace _converse.ChatRoomView
* @memberOf _converse
*/
export const ChatRoomView = ChatBoxView.extend({
export const ChatRoomView = _converse.ChatBoxView.extend({
length: 300,
tagName: 'div',
className: 'chatbox chatroom hidden',
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment