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"; ...@@ -16,7 +16,7 @@ import "shared/registry.js";
*/ */
import "./plugins/autocomplete.js"; import "./plugins/autocomplete.js";
import "./plugins/bookmark-views.js"; // Views for XEP-0048 Bookmarks 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/controlbox/index.js"; // The control box
import "./plugins/dragresize.js"; // Allows chat boxes to be resized by dragging them import "./plugins/dragresize.js"; // Allows chat boxes to be resized by dragging them
import "./plugins/fullscreen.js"; 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));
}
});
/** import UserDetailsModal from 'modals/user-details.js';
* @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 log from '@converse/headless/log'; import log from '@converse/headless/log';
import tpl_chatbox from 'templates/chatbox.js'; import tpl_chatbox from 'templates/chatbox.js';
import tpl_chatbox_head from 'templates/chatbox_head.js'; import tpl_chatbox_head from 'templates/chatbox_head.js';
import tpl_chatbox_message_form from 'templates/chatbox_message_form.js'; import tpl_chatbox_message_form from 'templates/chatbox_message_form.js';
import tpl_spinner from 'templates/spinner.js'; import tpl_spinner from 'templates/spinner.js';
import tpl_toolbar from 'templates/toolbar.js'; import tpl_toolbar from 'templates/toolbar.js';
import UserDetailsModal from 'modals/user-details.js';
import { View } from '@converse/skeletor/src/view.js'; import { View } from '@converse/skeletor/src/view.js';
import { __ } from '../i18n'; import { __ } from '../../i18n';
import { _converse, api, converse } from '@converse/headless/core'; import { _converse, api, converse } from '@converse/headless/core';
import { debounce } from 'lodash-es'; import { debounce } from 'lodash-es';
import { html, render } from 'lit-html'; import { html, render } from 'lit-html';
const { Strophe, dayjs } = converse.env;
const u = converse.env.utils; const u = converse.env.utils;
const { dayjs } = converse.env;
/** /**
* The View of an open/ongoing chat conversation. * The View of an open/ongoing chat conversation.
...@@ -32,10 +20,10 @@ const u = converse.env.utils; ...@@ -32,10 +20,10 @@ const u = converse.env.utils;
* @namespace _converse.ChatBoxView * @namespace _converse.ChatBoxView
* @memberOf _converse * @memberOf _converse
*/ */
export const ChatBoxView = View.extend({ const ChatBoxView = View.extend({
length: 200, length: 200,
className: 'chatbox hidden', className: 'chatbox hidden',
is_chatroom: false, // Leaky abstraction from MUC is_chatroom: false, // Leaky abstraction from MUC
events: { events: {
'click .chatbox-navback': 'showControlBox', 'click .chatbox-navback': 'showControlBox',
...@@ -45,14 +33,14 @@ export const ChatBoxView = View.extend({ ...@@ -45,14 +33,14 @@ export const ChatBoxView = View.extend({
'input .chat-textarea': 'inputChanged', 'input .chat-textarea': 'inputChanged',
'keydown .chat-textarea': 'onKeyDown', 'keydown .chat-textarea': 'onKeyDown',
'keyup .chat-textarea': 'onKeyUp', 'keyup .chat-textarea': 'onKeyUp',
'paste .chat-textarea': 'onPaste', 'paste .chat-textarea': 'onPaste'
}, },
async initialize () { async initialize () {
this.initDebounced(); this.initDebounced();
this.listenTo(this.model, 'change:composing_spoiler', this.renderMessageForm); 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, 'change:status', this.onStatusMessageChanged);
this.listenTo(this.model, 'destroy', this.remove); this.listenTo(this.model, 'destroy', this.remove);
this.listenTo(this.model, 'show', this.show); this.listenTo(this.model, 'show', this.show);
...@@ -108,9 +96,7 @@ export const ChatBoxView = View.extend({ ...@@ -108,9 +96,7 @@ export const ChatBoxView = View.extend({
}, },
render () { render () {
const result = tpl_chatbox( const result = tpl_chatbox(Object.assign(this.model.toJSON(), { 'markScrolled': ev => this.markScrolled(ev) }));
Object.assign(this.model.toJSON(), {'markScrolled': ev => this.markScrolled(ev)})
);
render(result, this.el); render(result, this.el);
this.content = this.el.querySelector('.chat-content'); this.content = this.el.querySelector('.chat-content');
this.notifications = this.el.querySelector('.chat-content__notifications'); this.notifications = this.el.querySelector('.chat-content__notifications');
...@@ -161,41 +147,40 @@ export const ChatBoxView = View.extend({ ...@@ -161,41 +147,40 @@ export const ChatBoxView = View.extend({
renderHelpMessages () { renderHelpMessages () {
render( render(
html`<converse-chat-help html`
.model=${this.model} <converse-chat-help
.messages=${this.getHelpMessages()} .model=${this.model}
?hidden=${!this.model.get('show_help_messages')} .messages=${this.getHelpMessages()}
type="info" ?hidden=${!this.model.get('show_help_messages')}
chat_type="${this.model.get('type')}"></converse-chat-help>`, type="info"
chat_type="${this.model.get('type')}"
></converse-chat-help>
`,
this.help_container this.help_container
); );
}, },
renderChatContent (msgs_by_ref=false) { renderChatContent (msgs_by_ref = false) {
if (!this.tpl_chat_content) { if (!this.tpl_chat_content) {
this.tpl_chat_content = (o) => { this.tpl_chat_content = o => {
return html` return html`
<converse-chat-content <converse-chat-content .chatview=${this} .messages=${o.messages} notifications=${o.notifications}>
.chatview=${this} </converse-chat-content>
.messages=${o.messages} `;
notifications=${o.notifications}>
</converse-chat-content>`
}; };
} }
const msg_models = this.model.messages.models; const msg_models = this.model.messages.models;
const messages = msgs_by_ref ? msg_models : Array.from(msg_models); const messages = msgs_by_ref ? msg_models : Array.from(msg_models);
render( render(this.tpl_chat_content({ messages, 'notifications': this.getNotifications() }), this.msgs_container);
this.tpl_chat_content({ messages, 'notifications': this.getNotifications() }),
this.msgs_container
);
}, },
renderToolbar () { renderToolbar () {
if (!api.settings.get('show_toolbar')) { if (!api.settings.get('show_toolbar')) {
return this; return this;
} }
const options = Object.assign({ const options = Object.assign(
{
'model': this.model, 'model': this.model,
'chatview': this 'chatview': this
}, },
...@@ -215,16 +200,20 @@ export const ChatBoxView = View.extend({ ...@@ -215,16 +200,20 @@ export const ChatBoxView = View.extend({
renderMessageForm () { renderMessageForm () {
const form_container = this.el.querySelector('.message-form-container'); const form_container = this.el.querySelector('.message-form-container');
render(tpl_chatbox_message_form( render(
Object.assign(this.model.toJSON(), { tpl_chatbox_message_form(
'hint_value': this.el.querySelector('.spoiler-hint')?.value, Object.assign(this.model.toJSON(), {
'label_message': this.model.get('composing_spoiler') ? __('Hidden message') : __('Message'), 'hint_value': this.el.querySelector('.spoiler-hint')?.value,
'label_spoiler_hint': __('Optional hint'), 'label_message': this.model.get('composing_spoiler') ? __('Hidden message') : __('Message'),
'message_value': this.el.querySelector('.chat-textarea')?.value, 'label_spoiler_hint': __('Optional hint'),
'show_send_button': api.settings.get('show_send_button'), 'message_value': this.el.querySelector('.chat-textarea')?.value,
'show_toolbar': api.settings.get('show_toolbar'), 'show_send_button': api.settings.get('show_send_button'),
'unread_msgs': __('You have unread messages') 'show_toolbar': api.settings.get('show_toolbar'),
})), form_container); 'unread_msgs': __('You have unread messages')
})
),
form_container
);
this.el.addEventListener('focusin', ev => this.emitFocused(ev)); this.el.addEventListener('focusin', ev => this.emitFocused(ev));
this.el.addEventListener('focusout', ev => this.emitBlurred(ev)); this.el.addEventListener('focusout', ev => this.emitBlurred(ev));
this.renderToolbar(); this.renderToolbar();
...@@ -238,7 +227,7 @@ export const ChatBoxView = View.extend({ ...@@ -238,7 +227,7 @@ export const ChatBoxView = View.extend({
showUserDetailsModal (ev) { showUserDetailsModal (ev) {
ev.preventDefault(); ev.preventDefault();
api.modal.show(UserDetailsModal, {model: this.model}, ev); api.modal.show(UserDetailsModal, { model: this.model }, ev);
}, },
onDragOver (evt) { onDragOver (evt) {
...@@ -262,43 +251,49 @@ export const ChatBoxView = View.extend({ ...@@ -262,43 +251,49 @@ export const ChatBoxView = View.extend({
async getHeadingStandaloneButton (promise_or_data) { async getHeadingStandaloneButton (promise_or_data) {
const data = await promise_or_data; const data = await promise_or_data;
return html`<a href="#" return html`
class="chatbox-btn ${data.a_class} fa ${data.icon_class}" <a
@click=${data.handler} href="#"
title="${data.i18n_title}"></a>`; class="chatbox-btn ${data.a_class} fa ${data.icon_class}"
@click=${data.handler}
title="${data.i18n_title}"
></a>
`;
}, },
async getHeadingDropdownItem (promise_or_data) { async getHeadingDropdownItem (promise_or_data) {
const data = await promise_or_data; const data = await promise_or_data;
return html`<a href="#" return html`
class="dropdown-item ${data.a_class}" <a href="#" class="dropdown-item ${data.a_class}" @click=${data.handler} title="${data.i18n_title}"
@click=${data.handler} ><i class="fa ${data.icon_class}"></i>${data.i18n_text}</a
title="${data.i18n_title}"><i class="fa ${data.icon_class}"></i>${data.i18n_text}</a>`; >
`;
}, },
async generateHeadingTemplate () { async generateHeadingTemplate () {
const vcard = this.model?.vcard; const vcard = this.model?.vcard;
const vcard_json = vcard ? vcard.toJSON() : {}; const vcard_json = vcard ? vcard.toJSON() : {};
const i18n_profile = __('The User\'s Profile Image'); const i18n_profile = __("The User's Profile Image");
const avatar_data = Object.assign({ const avatar_data = Object.assign(
'alt_text': i18n_profile, {
'extra_classes': '', 'alt_text': i18n_profile,
'height': 40, 'extra_classes': '',
'width': 40, 'height': 40,
}, vcard_json); 'width': 40
},
vcard_json
);
const heading_btns = await this.getHeadingButtons(); const heading_btns = await this.getHeadingButtons();
const standalone_btns = heading_btns.filter(b => b.standalone); const standalone_btns = heading_btns.filter(b => b.standalone);
const dropdown_btns = heading_btns.filter(b => !b.standalone); const dropdown_btns = heading_btns.filter(b => !b.standalone);
return tpl_chatbox_head( return tpl_chatbox_head(
Object.assign( Object.assign(this.model.toJSON(), {
this.model.toJSON(), { avatar_data,
avatar_data, 'display_name': this.model.getDisplayName(),
'display_name': this.model.getDisplayName(), 'dropdown_btns': dropdown_btns.map(b => this.getHeadingDropdownItem(b)),
'dropdown_btns': dropdown_btns.map(b => this.getHeadingDropdownItem(b)), 'showUserDetailsModal': ev => this.showUserDetailsModal(ev),
'showUserDetailsModal': ev => this.showUserDetailsModal(ev), 'standalone_btns': standalone_btns.map(b => this.getHeadingStandaloneButton(b))
'standalone_btns': standalone_btns.map(b => this.getHeadingStandaloneButton(b)), })
}
)
); );
}, },
...@@ -310,16 +305,18 @@ export const ChatBoxView = View.extend({ ...@@ -310,16 +305,18 @@ export const ChatBoxView = View.extend({
* @method _converse.ChatBoxView#getHeadingButtons * @method _converse.ChatBoxView#getHeadingButtons
*/ */
getHeadingButtons () { getHeadingButtons () {
const buttons = [{ const buttons = [
'a_class': 'show-user-details-modal', {
'handler': ev => this.showUserDetailsModal(ev), 'a_class': 'show-user-details-modal',
'i18n_text': __('Details'), 'handler': ev => this.showUserDetailsModal(ev),
'i18n_title': __('See more information about this person'), 'i18n_text': __('Details'),
'icon_class': 'fa-id-card', 'i18n_title': __('See more information about this person'),
'name': 'details', 'icon_class': 'fa-id-card',
'standalone': api.settings.get("view_mode") === 'overlayed', 'name': 'details',
}]; 'standalone': api.settings.get('view_mode') === 'overlayed'
if (!api.settings.get("singleton")) { }
];
if (!api.settings.get('singleton')) {
buttons.push({ buttons.push({
'a_class': 'close-chatbox-button', 'a_class': 'close-chatbox-button',
'handler': ev => this.close(ev), 'handler': ev => this.close(ev),
...@@ -327,7 +324,7 @@ export const ChatBoxView = View.extend({ ...@@ -327,7 +324,7 @@ export const ChatBoxView = View.extend({
'i18n_title': __('Close and end this conversation'), 'i18n_title': __('Close and end this conversation'),
'icon_class': 'fa-times', 'icon_class': 'fa-times',
'name': 'close', '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({ ...@@ -364,7 +361,7 @@ export const ChatBoxView = View.extend({
* - An optional message that serves as the cause for needing to scroll down. * - An optional message that serves as the cause for needing to scroll down.
*/ */
maybeScrollDown (message) { 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()) { if ((new_own_msg || !this.model.get('scrolled')) && !this.model.isHidden()) {
this.debouncedScrollDown(); this.debouncedScrollDown();
} }
...@@ -383,12 +380,12 @@ export const ChatBoxView = View.extend({ ...@@ -383,12 +380,12 @@ export const ChatBoxView = View.extend({
if (this.model.get('scrolled')) { if (this.model.get('scrolled')) {
u.safeSave(this.model, { u.safeSave(this.model, {
'scrolled': false, 'scrolled': false,
'scrollTop': null, 'scrollTop': null
}); });
} }
if (this.msgs_container.scrollTo) { if (this.msgs_container.scrollTo) {
const behavior = this.msgs_container.scrollTop ? 'smooth' : 'auto'; 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 { } else {
this.msgs_container.scrollTop = this.msgs_container.scrollHeight; this.msgs_container.scrollTop = this.msgs_container.scrollHeight;
} }
...@@ -420,7 +417,7 @@ export const ChatBoxView = View.extend({ ...@@ -420,7 +417,7 @@ export const ChatBoxView = View.extend({
return this; return this;
}, },
addSpinner (append=false) { addSpinner (append = false) {
if (this.el.querySelector('.spinner') === null) { if (this.el.querySelector('.spinner') === null) {
const el = u.getElementFromTemplateResult(tpl_spinner()); const el = u.getElementFromTemplateResult(tpl_spinner());
if (append) { if (append) {
...@@ -472,19 +469,28 @@ export const ChatBoxView = View.extend({ ...@@ -472,19 +469,28 @@ export const ChatBoxView = View.extend({
const date = dayjs(el.getAttribute('data-isodate')); const date = dayjs(el.getAttribute('data-isodate'));
const next_el = el.nextElementSibling; const next_el = el.nextElementSibling;
if (!u.hasClass('chat-msg--action', el) && !u.hasClass('chat-msg--action', previous_el) && if (
!u.hasClass('chat-info', el) && !u.hasClass('chat-info', previous_el) && !u.hasClass('chat-msg--action', el) &&
previous_el.getAttribute('data-from') === from && !u.hasClass('chat-msg--action', previous_el) &&
date.isBefore(dayjs(previous_el.getAttribute('data-isodate')).add(10, 'minutes')) && !u.hasClass('chat-info', el) &&
el.getAttribute('data-encrypted') === previous_el.getAttribute('data-encrypted')) { !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); 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) && if (
next_el.getAttribute('data-from') === from && !u.hasClass('chat-msg--action', el) &&
dayjs(next_el.getAttribute('data-isodate')).isBefore(date.add(10, 'minutes')) && u.hasClass('chat-info', el) &&
el.getAttribute('data-encrypted') === next_el.getAttribute('data-encrypted')) { 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); u.addClass('chat-msg--followup', next_el);
} else { } else {
u.removeClass('chat-msg--followup', next_el); u.removeClass('chat-msg--followup', next_el);
...@@ -492,16 +498,16 @@ export const ChatBoxView = View.extend({ ...@@ -492,16 +498,16 @@ export const ChatBoxView = View.extend({
}, },
parseMessageForCommands (text) { parseMessageForCommands (text) {
const match = text.replace(/^\s*/, "").match(/^\/(.*)\s*$/); const match = text.replace(/^\s*/, '').match(/^\/(.*)\s*$/);
if (match) { if (match) {
if (match[1] === "clear") { if (match[1] === 'clear') {
this.clearMessages(); this.clearMessages();
return true; return true;
} else if (match[1] === "close") { } else if (match[1] === 'close') {
this.close(); this.close();
return true; return true;
} else if (match[1] === "help") { } else if (match[1] === 'help') {
this.model.set({'show_help_messages': true}); this.model.set({ 'show_help_messages': true });
return true; return true;
} }
} }
...@@ -511,8 +517,10 @@ export const ChatBoxView = View.extend({ ...@@ -511,8 +517,10 @@ export const ChatBoxView = View.extend({
ev.preventDefault(); ev.preventDefault();
const textarea = this.el.querySelector('.chat-textarea'); const textarea = this.el.querySelector('.chat-textarea');
const message_text = textarea.value.trim(); const message_text = textarea.value.trim();
if (api.settings.get('message_limit') && message_text.length > api.settings.get('message_limit') || if (
!message_text.replace(/\s/g, '').length) { (api.settings.get('message_limit') && message_text.length > api.settings.get('message_limit')) ||
!message_text.replace(/\s/g, '').length
) {
return; return;
} }
if (!_converse.connection.authenticated) { if (!_converse.connection.authenticated) {
...@@ -521,7 +529,8 @@ export const ChatBoxView = View.extend({ ...@@ -521,7 +529,8 @@ export const ChatBoxView = View.extend({
api.connection.reconnect(); api.connection.reconnect();
return; return;
} }
let spoiler_hint, hint_el = {}; let spoiler_hint,
hint_el = {};
if (this.model.get('composing_spoiler')) { if (this.model.get('composing_spoiler')) {
hint_el = this.el.querySelector('form.sendXMPPMessage input.spoiler-hint'); hint_el = this.el.querySelector('form.sendXMPPMessage input.spoiler-hint');
spoiler_hint = hint_el.value; spoiler_hint = hint_el.value;
...@@ -548,7 +557,7 @@ export const ChatBoxView = View.extend({ ...@@ -548,7 +557,7 @@ export const ChatBoxView = View.extend({
*/ */
api.trigger('messageSend', message); 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 // XXX: Chrome flexbug workaround. The .chat-content area
// doesn't resize when the textarea is resized to its original size. // doesn't resize when the textarea is resized to its original size.
this.msgs_container.parentElement.style.display = 'none'; this.msgs_container.parentElement.style.display = 'none';
...@@ -556,13 +565,13 @@ export const ChatBoxView = View.extend({ ...@@ -556,13 +565,13 @@ export const ChatBoxView = View.extend({
textarea.removeAttribute('disabled'); textarea.removeAttribute('disabled');
u.removeClass('disabled', textarea); u.removeClass('disabled', textarea);
if (api.settings.get("view_mode") === 'overlayed') { if (api.settings.get('view_mode') === 'overlayed') {
// XXX: Chrome flexbug workaround. // XXX: Chrome flexbug workaround.
this.msgs_container.parentElement.style.display = ''; this.msgs_container.parentElement.style.display = '';
} }
// Suppress events, otherwise superfluous CSN gets set // Suppress events, otherwise superfluous CSN gets set
// immediately after the message, causing rate-limiting issues. // immediately after the message, causing rate-limiting issues.
this.model.setChatState(_converse.ACTIVE, {'silent': true}); this.model.setChatState(_converse.ACTIVE, { 'silent': true });
textarea.focus(); textarea.focus();
}, },
...@@ -652,17 +661,23 @@ export const ChatBoxView = View.extend({ ...@@ -652,17 +661,23 @@ export const ChatBoxView = View.extend({
if (!textarea.value || u.hasClass('correcting', textarea)) { if (!textarea.value || u.hasClass('correcting', textarea)) {
return this.editEarlierMessage(); return this.editEarlierMessage();
} }
} else if (ev.keyCode === converse.keycodes.DOWN_ARROW && } else if (
ev.target.selectionEnd === ev.target.value.length && ev.keyCode === converse.keycodes.DOWN_ARROW &&
u.hasClass('correcting', this.el.querySelector('.chat-textarea'))) { ev.target.selectionEnd === ev.target.value.length &&
u.hasClass('correcting', this.el.querySelector('.chat-textarea'))
) {
return this.editLaterMessage(); return this.editLaterMessage();
} }
} }
if ([converse.keycodes.SHIFT, if (
[
converse.keycodes.SHIFT,
converse.keycodes.META, converse.keycodes.META,
converse.keycodes.META_RIGHT, converse.keycodes.META_RIGHT,
converse.keycodes.ESCAPE, converse.keycodes.ESCAPE,
converse.keycodes.ALT].includes(ev.keyCode)) { converse.keycodes.ALT
].includes(ev.keyCode)
) {
return; return;
} }
if (this.model.get('chat_state') !== _converse.COMPOSING) { if (this.model.get('chat_state') !== _converse.COMPOSING) {
...@@ -673,7 +688,7 @@ export const ChatBoxView = View.extend({ ...@@ -673,7 +688,7 @@ export const ChatBoxView = View.extend({
}, },
getOwnMessages () { getOwnMessages () {
return this.model.messages.filter({'sender': 'me'}); return this.model.messages.filter({ 'sender': 'me' });
}, },
onEnterPressed (ev) { onEnterPressed (ev) {
...@@ -683,7 +698,7 @@ export const ChatBoxView = View.extend({ ...@@ -683,7 +698,7 @@ export const ChatBoxView = View.extend({
onEscapePressed (ev) { onEscapePressed (ev) {
ev.preventDefault(); ev.preventDefault();
const idx = this.model.messages.findLastIndex('correcting'); 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) { if (message) {
message.save('correcting', false); message.save('correcting', false);
} }
...@@ -694,10 +709,11 @@ export const ChatBoxView = View.extend({ ...@@ -694,10 +709,11 @@ export const ChatBoxView = View.extend({
if (message.get('sender') !== 'me') { if (message.get('sender') !== 'me') {
return log.error("onMessageRetractButtonClicked called for someone else's message!"); return log.error("onMessageRetractButtonClicked called for someone else's message!");
} }
const retraction_warning = const retraction_warning = __(
__("Be aware that other XMPP/Jabber clients (and servers) may "+ 'Be aware that other XMPP/Jabber clients (and servers) may ' +
"not yet support retractions and that this message may not "+ 'not yet support retractions and that this message may not ' +
"be removed everywhere."); 'be removed everywhere.'
);
const messages = [__('Are you sure you want to retract this message?')]; const messages = [__('Are you sure you want to retract this message?')];
if (api.settings.get('show_retraction_warning')) { if (api.settings.get('show_retraction_warning')) {
...@@ -713,7 +729,7 @@ export const ChatBoxView = View.extend({ ...@@ -713,7 +729,7 @@ export const ChatBoxView = View.extend({
const currently_correcting = this.model.messages.findWhere('correcting'); const currently_correcting = this.model.messages.findWhere('correcting');
const unsent_text = this.el.querySelector('.chat-textarea')?.value; const unsent_text = this.el.querySelector('.chat-textarea')?.value;
if (unsent_text && (!currently_correcting || currently_correcting.get('message') !== unsent_text)) { 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; return;
} }
} }
...@@ -733,7 +749,7 @@ export const ChatBoxView = View.extend({ ...@@ -733,7 +749,7 @@ export const ChatBoxView = View.extend({
let idx = this.model.messages.findLastIndex('correcting'); let idx = this.model.messages.findLastIndex('correcting');
if (idx >= 0) { if (idx >= 0) {
this.model.messages.at(idx).save('correcting', false); this.model.messages.at(idx).save('correcting', false);
while (idx < this.model.messages.length-1) { while (idx < this.model.messages.length - 1) {
idx += 1; idx += 1;
const candidate = this.model.messages.at(idx); const candidate = this.model.messages.at(idx);
if (candidate.get('editable')) { if (candidate.get('editable')) {
...@@ -764,7 +780,11 @@ export const ChatBoxView = View.extend({ ...@@ -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) { if (message) {
this.insertIntoTextArea(u.prefixMentions(message), true, true); this.insertIntoTextArea(u.prefixMentions(message), true, true);
message.save('correcting', true); message.save('correcting', true);
...@@ -780,8 +800,10 @@ export const ChatBoxView = View.extend({ ...@@ -780,8 +800,10 @@ export const ChatBoxView = View.extend({
}, },
async clearMessages (ev) { async clearMessages (ev) {
if (ev && ev.preventDefault) { ev.preventDefault(); } if (ev && ev.preventDefault) {
const result = confirm(__("Are you sure you want to clear the messages from this conversation?")); ev.preventDefault();
}
const result = confirm(__('Are you sure you want to clear the messages from this conversation?'));
if (result === true) { if (result === true) {
await this.model.clearMessages(); await this.model.clearMessages();
} }
...@@ -800,7 +822,7 @@ export const ChatBoxView = View.extend({ ...@@ -800,7 +822,7 @@ export const ChatBoxView = View.extend({
* @param {integer} [position] - The end index of the string to be * @param {integer} [position] - The end index of the string to be
* replaced with the new value. * 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'); const textarea = this.el.querySelector('.chat-textarea');
if (correcting) { if (correcting) {
u.addClass('correcting', textarea); u.addClass('correcting', textarea);
...@@ -809,19 +831,18 @@ export const ChatBoxView = View.extend({ ...@@ -809,19 +831,18 @@ export const ChatBoxView = View.extend({
} }
if (replace) { if (replace) {
if (position && typeof replace == 'string') { if (position && typeof replace == 'string') {
textarea.value = textarea.value.replace( textarea.value = textarea.value.replace(new RegExp(replace, 'g'), (match, offset) =>
new RegExp(replace, 'g'), offset == position - replace.length ? value + ' ' : match
(match, offset) => (offset == position-replace.length ? value+' ' : match)
); );
} else { } else {
textarea.value = value; textarea.value = value;
} }
} else { } else {
let existing = textarea.value; let existing = textarea.value;
if (existing && (existing[existing.length-1] !== ' ')) { if (existing && existing[existing.length - 1] !== ' ') {
existing = existing + ' '; existing = existing + ' ';
} }
textarea.value = existing+value+' '; textarea.value = existing + value + ' ';
} }
this.updateCharCounter(textarea.value); this.updateCharCounter(textarea.value);
u.placeCaretAtEnd(textarea); u.placeCaretAtEnd(textarea);
...@@ -837,18 +858,20 @@ export const ChatBoxView = View.extend({ ...@@ -837,18 +858,20 @@ export const ChatBoxView = View.extend({
text = __('%1$s has gone offline', fullname); text = __('%1$s has gone offline', fullname);
} else if (show === 'away') { } else if (show === 'away') {
text = __('%1$s has gone away', fullname); text = __('%1$s has gone away', fullname);
} else if ((show === 'dnd')) { } else if (show === 'dnd') {
text = __('%1$s is busy', fullname); text = __('%1$s is busy', fullname);
} else if (show === 'online') { } else if (show === 'online') {
text = __('%1$s is online', fullname); 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) { async close (ev) {
if (ev && ev.preventDefault) { ev.preventDefault(); } if (ev && ev.preventDefault) {
if (_converse.router.history.getFragment() === "converse/chat?jid="+this.model.get('jid')) { ev.preventDefault();
}
if (_converse.router.history.getFragment() === 'converse/chat?jid=' + this.model.get('jid')) {
_converse.router.navigate(''); _converse.router.navigate('');
} }
if (api.connection.connected()) { if (api.connection.connected()) {
...@@ -963,8 +986,7 @@ export const ChatBoxView = View.extend({ ...@@ -963,8 +986,7 @@ export const ChatBoxView = View.extend({
let scrolled = true; let scrolled = true;
let scrollTop = null; let scrollTop = null;
const is_at_bottom = const is_at_bottom =
(this.msgs_container.scrollTop + this.msgs_container.clientHeight) >= this.msgs_container.scrollTop + this.msgs_container.clientHeight >= this.msgs_container.scrollHeight - 62; // sigh...
this.msgs_container.scrollHeight - 62; // sigh...
if (is_at_bottom) { if (is_at_bottom) {
scrolled = false; scrolled = false;
...@@ -984,7 +1006,7 @@ export const ChatBoxView = View.extend({ ...@@ -984,7 +1006,7 @@ export const ChatBoxView = View.extend({
}, },
viewUnreadMessages () { viewUnreadMessages () {
this.model.save({'scrolled': false, 'scrollTop': null}); this.model.save({ 'scrolled': false, 'scrollTop': null });
this.scrollDown(); this.scrollDown();
}, },
...@@ -1003,7 +1025,7 @@ export const ChatBoxView = View.extend({ ...@@ -1003,7 +1025,7 @@ export const ChatBoxView = View.extend({
* @property { _converse.ChatBox | _converse.ChatRoom } chatbox - The chat model * @property { _converse.ChatBox | _converse.ChatRoom } chatbox - The chat model
* @example _converse.api.listen.on('chatBoxScrolledDown', obj => { ... }); * @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) { onWindowStateChanged (state) {
...@@ -1015,118 +1037,10 @@ export const ChatBoxView = View.extend({ ...@@ -1015,118 +1037,10 @@ export const ChatBoxView = View.extend({
} }
} }
} else if (state === 'hidden') { } else if (state === 'hidden') {
this.model.setChatState(_converse.INACTIVE, {'silent': true}); this.model.setChatState(_converse.INACTIVE, { 'silent': true });
this.model.sendChatState(); this.model.sendChatState();
} }
} }
}); });
export default ChatBoxView;
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 ************************/
}
});
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
* @license Mozilla Public License (MPLv2) * @license Mozilla Public License (MPLv2)
*/ */
import "../../components/brand-heading"; import "../../components/brand-heading";
import "../chatview"; import "../chatview/index.js";
import ControlBoxMixin from './model.js'; import ControlBoxMixin from './model.js';
import ControlBoxPane from './pane.js'; import ControlBoxPane from './pane.js';
import ControlBoxToggle from './toggle.js'; import ControlBoxToggle from './toggle.js';
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
* @copyright 2020, the Converse.js contributors * @copyright 2020, the Converse.js contributors
* @license Mozilla Public License (MPLv2) * @license Mozilla Public License (MPLv2)
*/ */
import "./chatview.js"; import "./chatview/index.js";
import "./controlbox/index.js"; import "./controlbox/index.js";
import { debounce } from "lodash-es"; import { debounce } from "lodash-es";
import { _converse, api, converse } from "@converse/headless/core"; import { _converse, api, converse } from "@converse/headless/core";
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
* @license Mozilla Public License (MPLv2) * @license Mozilla Public License (MPLv2)
* @copyright 2020, the Converse.js contributors * @copyright 2020, the Converse.js contributors
*/ */
import "./chatview.js"; import "./chatview/index.js";
import "./controlbox/index.js"; import "./controlbox/index.js";
import "./singleton.js"; import "./singleton.js";
import "@converse/headless/plugins/muc"; import "@converse/headless/plugins/muc";
......
...@@ -3,9 +3,9 @@ ...@@ -3,9 +3,9 @@
* @copyright 2020, the Converse.js contributors * @copyright 2020, the Converse.js contributors
* @license Mozilla Public License (MPLv2) * @license Mozilla Public License (MPLv2)
*/ */
import "./chatview/index.js";
import tpl_chatbox from "../templates/chatbox.js"; import tpl_chatbox from "../templates/chatbox.js";
import tpl_headline_panel from "../templates/headline_panel.js"; import tpl_headline_panel from "../templates/headline_panel.js";
import { ChatBoxView } from "./chatview";
import { View } from '@converse/skeletor/src/view.js'; import { View } from '@converse/skeletor/src/view.js';
import { __ } from '../i18n'; import { __ } from '../i18n';
import { _converse, api, converse } from "@converse/headless/core"; import { _converse, api, converse } from "@converse/headless/core";
...@@ -14,7 +14,7 @@ import { render } from "lit-html"; ...@@ -14,7 +14,7 @@ import { render } from "lit-html";
const u = converse.env.utils; const u = converse.env.utils;
const HeadlinesBoxView = ChatBoxView.extend({ const HeadlinesBoxViewMixin = {
className: 'chatbox headlines hidden', className: 'chatbox headlines hidden',
events: { events: {
...@@ -100,10 +100,10 @@ const HeadlinesBoxView = ChatBoxView.extend({ ...@@ -100,10 +100,10 @@ const HeadlinesBoxView = ChatBoxView.extend({
return _converse.api.hook('getHeadingButtons', this, buttons); 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 () {}, 'renderMessageForm': function renderMessageForm () {},
'afterShown': function afterShown () {} 'afterShown': function afterShown () {}
}); };
/** /**
...@@ -210,7 +210,7 @@ converse.plugins.add('converse-headlines-view', { ...@@ -210,7 +210,7 @@ converse.plugins.add('converse-headlines-view', {
Object.assign(_converse.ControlBoxView.prototype, viewWithHeadlinesPanel); Object.assign(_converse.ControlBoxView.prototype, viewWithHeadlinesPanel);
} }
_converse.HeadlinesBoxView = HeadlinesBoxView; _converse.HeadlinesBoxView = _converse.ChatBoxView.extend(HeadlinesBoxViewMixin);
_converse.HeadlinesPanel = HeadlinesPanel; _converse.HeadlinesPanel = HeadlinesPanel;
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
* @license Mozilla Public License (MPLv2) * @license Mozilla Public License (MPLv2)
*/ */
import '../components/minimized_chat.js'; import '../components/minimized_chat.js';
import './chatview.js'; import './chatview/index.js';
import tpl_chats_panel from '../templates/chats_panel.js'; import tpl_chats_panel from '../templates/chats_panel.js';
import { Model } from '@converse/skeletor/src/model.js'; import { Model } from '@converse/skeletor/src/model.js';
import { View } from '@converse/skeletor/src/view'; import { View } from '@converse/skeletor/src/view';
......
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
* @license Mozilla Public License (MPLv2) * @license Mozilla Public License (MPLv2)
*/ */
import "../components/muc-sidebar"; import "../components/muc-sidebar";
import "./chatview/index.js";
import "./modal.js"; import "./modal.js";
import "@converse/headless/utils/muc"; import "@converse/headless/utils/muc";
import AddMUCModal from '../modals/add-muc.js'; import AddMUCModal from '../modals/add-muc.js';
...@@ -24,7 +25,6 @@ import tpl_muc_nickname_form from "../templates/muc_nickname_form.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_muc_password_form from "../templates/muc_password_form.js";
import tpl_room_panel from "../templates/room_panel.js"; import tpl_room_panel from "../templates/room_panel.js";
import tpl_spinner from "../templates/spinner.js"; import tpl_spinner from "../templates/spinner.js";
import { ChatBoxView } from "./chatview.js";
import { Model } from '@converse/skeletor/src/model.js'; import { Model } from '@converse/skeletor/src/model.js';
import { View } from '@converse/skeletor/src/view.js'; import { View } from '@converse/skeletor/src/view.js';
import { __ } from '../i18n'; import { __ } from '../i18n';
...@@ -63,7 +63,7 @@ const COMMAND_TO_AFFILIATION = { ...@@ -63,7 +63,7 @@ const COMMAND_TO_AFFILIATION = {
* @namespace _converse.ChatRoomView * @namespace _converse.ChatRoomView
* @memberOf _converse * @memberOf _converse
*/ */
export const ChatRoomView = ChatBoxView.extend({ export const ChatRoomView = _converse.ChatBoxView.extend({
length: 300, length: 300,
tagName: 'div', tagName: 'div',
className: 'chatbox chatroom hidden', 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