xmpp.chapril.org-conversejs/src/converse-chatview.js

1237 lines
55 KiB
JavaScript
Raw Normal View History

2016-03-13 17:16:53 +01:00
// Converse.js (A browser based XMPP chat client)
// http://conversejs.org
//
2017-02-02 19:29:38 +01:00
// Copyright (c) 2012-2017, Jan-Carel Brand <jc@opkode.com>
2016-03-13 17:16:53 +01:00
// Licensed under the Mozilla Public License (MPLv2)
//
/*global define */
2016-03-13 17:16:53 +01:00
(function (root, factory) {
define([
"converse-core",
"converse-chatboxes",
2017-06-16 11:31:57 +02:00
"emojione",
"xss",
"tpl!action",
"tpl!chatbox",
"tpl!chatbox_head",
2018-02-04 18:33:39 +01:00
"tpl!chatbox_message_form",
2017-06-16 11:31:57 +02:00
"tpl!emojis",
2017-04-21 18:35:34 +02:00
"tpl!help_message",
"tpl!info",
"tpl!message",
"tpl!new_day",
"tpl!spinner",
"tpl!spoiler_button",
"tpl!spoiler_message",
"tpl!toolbar"
], factory);
}(this, function (
converse,
dummy,
2017-06-16 11:31:57 +02:00
emojione,
xss,
tpl_action,
tpl_chatbox,
tpl_chatbox_head,
2018-02-04 18:33:39 +01:00
tpl_chatbox_message_form,
2017-06-16 11:31:57 +02:00
tpl_emojis,
2017-04-21 18:35:34 +02:00
tpl_help_message,
tpl_info,
tpl_message,
tpl_new_day,
tpl_spinner,
tpl_spoiler_button,
tpl_spoiler_message,
tpl_toolbar
) {
2016-03-13 17:16:53 +01:00
"use strict";
2018-01-02 21:25:30 +01:00
const { $msg, Backbone, Strophe, _, b64_sha1, sizzle, moment } = converse.env;
const u = converse.env.utils;
const KEY = {
2016-03-13 17:16:53 +01:00
ENTER: 13,
FORWARD_SLASH: 47
};
converse.plugins.add('converse-chatview', {
/* Plugin dependencies are other plugins which might be
2018-01-03 17:08:30 +01:00
* overridden or relied upon, and therefore need to be loaded before
* this plugin.
2018-01-03 17:08:30 +01:00
*
* 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.
2018-01-03 17:08:30 +01:00
*
* NB: These plugins need to have already been loaded via require.js.
*/
dependencies: ["converse-chatboxes"],
2016-03-13 17:16:53 +01:00
overrides: {
// Overrides mentioned here will be picked up by converse.js's
// plugin architecture they will replace existing methods on the
// relevant objects or classes.
//
// New functions which don't exist yet can also be added.
2017-06-16 11:31:57 +02:00
//
registerGlobalEventHandlers: function () {
this.__super__.registerGlobalEventHandlers();
document.addEventListener(
2017-07-15 15:15:37 +02:00
'click', function (ev) {
if (_.includes(ev.target.classList, 'toggle-toolbar-menu') ||
_.includes(ev.target.classList, 'insert-emoji')) {
return;
}
u.slideInAllElements(
document.querySelectorAll('.toolbar-menu')
)
2017-06-16 11:31:57 +02:00
}
);
2017-06-16 11:31:57 +02:00
},
2016-03-13 17:16:53 +01:00
ChatBoxViews: {
onChatBoxAdded (item) {
const { _converse } = this.__super__;
let view = this.get(item.get('id'));
if (!view) {
view = new _converse.ChatBoxView({model: item});
2016-03-13 17:16:53 +01:00
this.add(item.get('id'), view);
return view;
2016-03-13 17:16:53 +01:00
} else {
2016-08-31 12:06:17 +02:00
return this.__super__.onChatBoxAdded.apply(this, arguments);
2016-03-13 17:16:53 +01:00
}
}
}
},
initialize () {
2016-03-13 17:16:53 +01:00
/* The initialize function gets called as soon as the plugin is
* loaded by converse.js's plugin machinery.
*/
const { _converse } = this,
{ __ } = _converse;
_converse.api.settings.update({
2018-02-04 18:33:39 +01:00
'allow_spoiler_messages': true,
'use_emojione': true,
'emojione_image_path': emojione.imagePathPNG,
'chatview_avatar_height': 32,
'chatview_avatar_width': 32,
'show_toolbar': true,
2018-01-18 11:41:47 +01:00
'show_message_load_animation': false,
'time_format': 'HH:mm',
'visible_toolbar_buttons': {
2017-06-24 11:00:44 +02:00
'emoji': true,
'call': false,
'clear': true
},
2016-03-13 17:16:53 +01:00
});
emojione.imagePathPNG = _converse.emojione_image_path;
emojione.ascii = true;
2016-03-13 17:16:53 +01:00
function onWindowStateChanged (data) {
_converse.chatboxviews.each(function (chatboxview) {
chatboxview.onWindowStateChanged(data.state);
});
}
_converse.api.listen.on('windowStateChanged', onWindowStateChanged);
2017-12-24 17:47:02 +01:00
_converse.EmojiPicker = Backbone.Model.extend({
2017-06-16 11:31:57 +02:00
defaults: {
'current_category': 'people',
'current_skintone': '',
'scroll_position': 0
},
initialize () {
const id = `converse.emoji-${_converse.bare_jid}`;
this.id = id;
this.browserStorage = new Backbone.BrowserStorage[_converse.storage](id);
2017-06-16 11:31:57 +02:00
}
});
_converse.EmojiPickerView = Backbone.NativeView.extend({
className: 'emoji-picker-container toolbar-menu collapsed',
2017-06-16 11:31:57 +02:00
events: {
'click .emoji-category-picker li.emoji-category': 'chooseCategory',
'click .emoji-skintone-picker li.emoji-skintone': 'chooseSkinTone'
2017-06-16 11:31:57 +02:00
},
initialize () {
this.model.on('change:current_skintone', this.render, this);
this.model.on('change:current_category', this.render, this);
this.setScrollPosition = _.debounce(this.setScrollPosition, 50);
2017-06-16 11:31:57 +02:00
},
render () {
2017-06-16 11:31:57 +02:00
var emojis_html = tpl_emojis(
_.extend(
this.model.toJSON(), {
'transform': _converse.use_emojione ? emojione.shortnameToImage : emojione.shortnameToUnicode,
'emojis_by_category': u.getEmojisByCategory(_converse, emojione),
'toned_emojis': u.getTonedEmojis(_converse),
'skintones': ['tone1', 'tone2', 'tone3', 'tone4', 'tone5'],
'shouldBeHidden': this.shouldBeHidden
2017-06-16 11:31:57 +02:00
}
));
this.el.innerHTML = emojis_html;
_.forEach(this.el.querySelectorAll('.emoji-picker'), (el) => {
el.addEventListener('scroll', this.setScrollPosition.bind(this));
});
this.restoreScrollPosition();
2017-06-16 11:31:57 +02:00
return this;
},
shouldBeHidden (shortname, current_skintone, toned_emojis) {
/* Helper method for the template which decides whether an
* emoji should be hidden, based on which skin tone is
* currently being applied.
*/
if (_.includes(shortname, '_tone')) {
if (!current_skintone || !_.includes(shortname, current_skintone)) {
return true;
}
} else {
if (current_skintone && _.includes(toned_emojis, shortname)) {
return true;
}
}
return false;
},
restoreScrollPosition () {
const current_picker = _.difference(
this.el.querySelectorAll('.emoji-picker'),
this.el.querySelectorAll('.emoji-picker.hidden')
);
if (current_picker.length === 1 && this.model.get('scroll_position')) {
current_picker[0].scrollTop = this.model.get('scroll_position');
}
},
setScrollPosition (ev) {
this.model.save('scroll_position', ev.target.scrollTop);
},
chooseSkinTone (ev) {
ev.preventDefault();
ev.stopPropagation();
const target = ev.target.nodeName === 'IMG' ?
ev.target.parentElement : ev.target;
const skintone = target.getAttribute("data-skintone").trim();
if (this.model.get('current_skintone') === skintone) {
this.model.save({'current_skintone': ''});
} else {
this.model.save({'current_skintone': skintone});
}
},
chooseCategory (ev) {
2017-06-16 11:31:57 +02:00
ev.preventDefault();
ev.stopPropagation();
const target = ev.target.nodeName === 'IMG' ?
ev.target.parentElement : ev.target;
const category = target.getAttribute("data-category").trim();
this.model.save({
'current_category': category,
'scroll_position': 0
});
2017-06-16 11:31:57 +02:00
}
});
_converse.ChatBoxHeading = Backbone.NativeView.extend({
initialize () {
2017-12-02 18:07:31 +01:00
this.model.on('change:image', this.render, this);
this.model.on('change:status', this.onStatusMessageChanged, this);
this.model.on('change:fullname', this.render, this);
},
render () {
this.el.innerHTML = tpl_chatbox_head(
_.extend(this.model.toJSON(), {
'avatar_width': _converse.chatview_avatar_width,
'avatar_height': _converse.chatview_avatar_height,
'info_close': __('Close this chat box'),
})
);
return this;
},
onStatusMessageChanged (item) {
this.render();
_converse.emit('contactStatusMessageChanged', {
'contact': item.attributes,
'message': item.get('status')
});
}
});
2018-02-04 19:31:02 +01:00
_converse.ChatBoxMessageForm = Backbone.VDOMView.extend({
className: 'message-form-container',
events: {
'click .toggle-spoiler-edit': 'toggleEditSpoilerMessage',
},
toHTML () {
let placeholder;
if (this.model.get('sending_spoiler')) {
placeholder = __('Spoiler message');
} else {
placeholder = __('Personal message');
}
return tpl_chatbox_message_form(
_.extend(this.model.toJSON(), {
'allow_spoiler_messages': _converse.allow_spoiler_messages,
'label_personal_message': placeholder,
'label_spoiler_hint': __('Optional hint'),
2018-02-04 19:31:02 +01:00
'label_send': __('Send'),
'show_send_button': _converse.show_send_button,
'show_textarea': true,
'show_toolbar': _converse.show_toolbar,
'hint_value': _.get(this.el.querySelector('.spoiler-hint'), 'value'),
'message_value': _.get(this.el.querySelector('.chat-textarea'), 'value')
2018-02-04 19:31:02 +01:00
}));
},
afterRender () {
this.renderToolbar();
},
2018-02-04 19:31:02 +01:00
renderToolbar (toolbar, options) {
if (!_converse.show_toolbar) {
return this;
}
toolbar = toolbar || tpl_toolbar;
options = _.assign(
this.model.toJSON(),
this.getToolbarOptions(options || {})
);
this.el.querySelector('.chat-toolbar').innerHTML = toolbar(options);
return this;
},
getToolbarOptions (options) {
return _.extend(options || {}, {
'allow_spoiler_messages': _converse.allow_spoiler_messages,
'label_clear': __('Clear all messages'),
'label_insert_smiley': __('Insert a smiley'),
'label_start_call': __('Start a call'),
'show_call_button': _converse.visible_toolbar_buttons.call,
'show_clear_button': _converse.visible_toolbar_buttons.clear,
'use_emoji': _converse.visible_toolbar_buttons.emoji,
});
},
focus () {
const textarea_el = this.el.querySelector('.chat-textarea');
if (!_.isNull(textarea_el)) {
textarea_el.focus();
_converse.emit('chatBoxFocused', this.parent);
}
return this;
},
2018-02-04 19:31:02 +01:00
toggleEditSpoilerMessage () {
const { __ } = _converse,
text_area = this.el.querySelector('.chat-textarea'),
spoiler_button = this.el.querySelector('.toggle-spoiler-edit');
let spoiler_title;
if (this.model.get('sending_spoiler')) {
this.model.set('sending_spoiler', false);
spoiler_title = __('Click to write your message as a spoiler');
} else {
this.model.set('sending_spoiler', true);
spoiler_title = __('Click to write as a normal (non-spoiler) message');
}
spoiler_button.outerHTML = tpl_spoiler_button(_.extend(
this.model.toJSON(), {'title': spoiler_title})
)
this.render();
this.focus();
2018-02-04 19:31:02 +01:00
}
});
_converse.ChatBoxView = Backbone.NativeView.extend({
2016-03-13 17:16:53 +01:00
length: 200,
className: 'chatbox hidden',
is_chatroom: false, // Leaky abstraction from MUC
2016-03-13 17:16:53 +01:00
events: {
'click .close-chatbox-button': 'close',
'click .new-msgs-indicator': 'viewUnreadMessages',
2017-07-15 15:58:11 +02:00
'click .send-button': 'onFormSubmitted',
2016-05-28 14:25:44 +02:00
'click .toggle-call': 'toggleCall',
'click .toggle-clear': 'clearMessages',
'click .toggle-smiley ul.emoji-picker li': 'insertEmoji',
'click .toggle-smiley': 'toggleEmojiMenu',
'click .toggle-spoiler': 'toggleSpoilerMessage',
'keypress .chat-textarea': 'keyPressed'
2016-03-13 17:16:53 +01:00
},
initialize () {
2018-01-02 21:44:46 +01:00
this.scrollDown = _.debounce(this._scrollDown, 250);
this.markScrolled = _.debounce(this._markScrolled, 100);
this.createEmojiPicker();
2016-03-13 17:16:53 +01:00
this.model.messages.on('add', this.onMessageAdded, this);
this.model.on('show', this.show, this);
this.model.on('destroy', this.remove, this);
2016-03-13 17:16:53 +01:00
// TODO check for changed fullname as well
this.model.on('change:chat_state', this.sendChatState, this);
this.model.on('change:chat_status', this.onChatStatusChanged, this);
this.model.on('showHelpMessages', this.showHelpMessages, this);
this.model.on('sendMessage', this.sendMessage, this);
2018-02-04 18:33:39 +01:00
this.render();
this.renderMessageForm();
this.insertHeading();
this.fetchMessages();
_converse.emit('chatBoxOpened', this);
_converse.emit('chatBoxInitialized', this);
2016-03-13 17:16:53 +01:00
},
render () {
this.el.setAttribute('id', this.model.get('box_id'));
this.el.innerHTML = tpl_chatbox(
_.extend(this.model.toJSON(), {
unread_msgs: __('You have unread messages')
}
));
this.content = this.el.querySelector('.chat-content');
return this;
},
2018-02-04 18:33:39 +01:00
renderMessageForm () {
2018-02-04 19:31:02 +01:00
this.message_form_view = new _converse.ChatBoxMessageForm({
'model': new Backbone.Model()
});
this.message_form_view.parent = this;
2018-02-04 19:31:02 +01:00
this.message_form_view.render();
this.content.insertAdjacentElement(
'afterEnd',
this.message_form_view.el
);
2018-02-02 20:53:10 +01:00
},
insertHeading () {
this.heading = new _converse.ChatBoxHeading({'model': this.model});
this.heading.render();
this.heading.chatview = this;
const flyout = this.el.querySelector('.flyout');
flyout.insertBefore(this.heading.el, flyout.querySelector('.chat-body'));
return this;
2016-03-13 17:16:53 +01:00
},
createEmojiPicker () {
if (_.isUndefined(_converse.emojipicker)) {
_converse.emojipicker = new _converse.EmojiPicker();
_converse.emojipicker.fetch();
}
this.emoji_picker_view = new _converse.EmojiPickerView({
'model': _converse.emojipicker
});
},
afterMessagesFetched () {
this.insertIntoDOM();
2017-02-25 22:17:14 +01:00
this.scrollDown();
this.content.addEventListener('scroll', this.markScrolled.bind(this));
_converse.emit('afterMessagesFetched', this);
2016-03-13 17:16:53 +01:00
},
fetchMessages () {
2016-03-13 17:16:53 +01:00
this.model.messages.fetch({
'add': true,
'success': this.afterMessagesFetched.bind(this),
'error': this.afterMessagesFetched.bind(this),
2016-03-13 17:16:53 +01:00
});
return this;
},
insertIntoDOM () {
/* This method gets overridden in src/converse-controlbox.js
* as well as src/converse-muc.js (if those plugins are
* enabled).
2016-03-19 23:16:00 +01:00
*/
const container = document.querySelector('#conversejs');
2017-02-25 22:17:14 +01:00
if (this.el.parentNode !== container) {
container.insertBefore(this.el, container.firstChild);
}
2016-03-13 17:16:53 +01:00
return this;
},
clearStatusNotification () {
2018-01-02 21:44:46 +01:00
u.removeElement(this.content.querySelector('.chat-event'));
2016-03-13 17:16:53 +01:00
},
showStatusNotification (message, keep_old, permanent) {
2016-03-13 17:16:53 +01:00
if (!keep_old) {
this.clearStatusNotification();
}
this.content.insertAdjacentHTML(
'beforeend',
tpl_info({
'extra_classes': !permanent ? 'chat-event' : '',
'message': message,
'isodate': moment().format(),
'data': ''
}));
this.scrollDown();
2016-03-13 17:16:53 +01:00
},
addSpinner (append=false) {
if (_.isNull(this.el.querySelector('.spinner'))) {
if (append) {
this.content.insertAdjacentHTML('beforeend', tpl_spinner());
this.scrollDown();
} else {
this.content.insertAdjacentHTML('afterbegin', tpl_spinner());
}
2016-03-13 17:16:53 +01:00
}
},
clearSpinner () {
_.each(
this.content.querySelectorAll('span.spinner'),
(el) => el.parentNode.removeChild(el)
);
2016-03-13 17:16:53 +01:00
},
2018-01-03 13:04:06 +01:00
insertDayIndicator (next_msg_el) {
/* Inserts an indicator into the chat area, showing the
* day as given by the passed in date.
2016-03-19 23:16:00 +01:00
*
* The indicator is only inserted if necessary.
*
2016-03-19 23:16:00 +01:00
* Parameters:
2018-01-03 13:04:06 +01:00
* (HTMLElement) next_msg_el - The message element before
* which the day indicator element must be inserted.
* This element must have a "data-isodate" attribute
* which specifies its creation date.
2016-03-19 23:16:00 +01:00
*/
const prev_msg_el = u.getPreviousElement(next_msg_el, ".message:not(.chat-event)"),
prev_msg_date = _.isNull(prev_msg_el) ? null : prev_msg_el.getAttribute('data-isodate'),
next_msg_date = next_msg_el.getAttribute('data-isodate');
if (_.isNull(prev_msg_date) || moment(next_msg_date).isAfter(prev_msg_date, 'day')) {
const day_date = moment(next_msg_date).startOf('day');
next_msg_el.insertAdjacentHTML('beforeBegin',
tpl_new_day({
'isodate': day_date.format(),
'datestring': day_date.format("dddd MMM Do YYYY")
})
);
}
},
2018-01-03 13:04:06 +01:00
2017-12-24 17:47:02 +01:00
getLastMessageDate (cutoff) {
/* Return the ISO8601 format date of the latest message.
*
* Parameters:
* (Object) cutoff: Moment Date cutoff date. The last
* message received cutoff this date will be returned.
*/
const first_msg = u.getFirstChildElement(this.content, '.message:not(.chat-event)'),
2018-01-02 21:25:30 +01:00
oldest_date = first_msg ? first_msg.getAttribute('data-isodate') : null;
if (!_.isNull(oldest_date) && moment(oldest_date).isAfter(cutoff)) {
return null;
}
const last_msg = u.getLastChildElement(this.content, '.message:not(.chat-event)'),
2018-01-02 21:25:30 +01:00
most_recent_date = last_msg ? last_msg.getAttribute('data-isodate') : null;
if (_.isNull(most_recent_date) || moment(most_recent_date).isBefore(cutoff)) {
return most_recent_date;
2017-12-24 17:47:02 +01:00
}
/* XXX: We avoid .chat-event messages, since they are
* temporary and get removed once a new element is
* inserted into the chat area, so we don't query for
* them here, otherwise we get a null reference later
* upon element insertion.
*/
2017-12-24 17:47:02 +01:00
const msg_dates = _.invokeMap(
sizzle('.message:not(.chat-event)', this.content),
2018-01-02 21:25:30 +01:00
Element.prototype.getAttribute, 'data-isodate'
2017-12-24 17:47:02 +01:00
)
if (_.isObject(cutoff)) {
cutoff = cutoff.format();
}
msg_dates.push(cutoff);
2017-12-24 17:47:02 +01:00
msg_dates.sort();
2018-01-02 21:25:30 +01:00
const idx = msg_dates.lastIndexOf(cutoff);
if (idx === 0) {
return null;
} else {
return msg_dates[idx-1];
}
},
showMessage (attrs) {
2016-03-13 17:16:53 +01:00
/* Inserts a chat message into the content area of the chat box.
2016-03-19 23:16:00 +01:00
* Will also insert a new day indicator if the message is on a
* different day.
*
* The message to show may either be newer than the newest
* message, or older than the oldest message.
*
* Parameters:
* (Object) attrs: An object containing the message
* attributes.
2016-03-19 23:16:00 +01:00
*/
2017-12-24 17:47:02 +01:00
const current_msg_date = moment(attrs.time) || moment,
2018-01-09 13:32:42 +01:00
previous_msg_date = this.getLastMessageDate(current_msg_date),
message_el = this.renderMessage(attrs);
2017-12-24 17:47:02 +01:00
2018-01-02 21:25:30 +01:00
if (_.isNull(previous_msg_date)) {
this.content.insertAdjacentElement('afterbegin', message_el);
} else {
2018-01-02 21:25:30 +01:00
const previous_msg_el = sizzle(`[data-isodate="${previous_msg_date}"]:last`, this.content).pop();
previous_msg_el.insertAdjacentElement('afterend', message_el);
}
this.insertDayIndicator(message_el);
this.clearStatusNotification();
this.setScrollPosition(message_el);
},
setScrollPosition (message_el) {
/* Given a newly inserted message, determine whether we
* should keep the scrollbar in place (so as to not scroll
* up when using infinite scroll).
*/
if (this.model.get('scrolled')) {
const next_msg_el = u.getNextElement(message_el, ".chat-message");
if (next_msg_el) {
// The currently received message is not new, there
// are newer messages after it. So let's see if we
// should maintain our current scroll position.
if (this.content.scrollTop === 0 || this.model.get('top_visible_message')) {
const top_visible_message = this.model.get('top_visible_message') || next_msg_el;
this.model.set('top_visible_message', top_visible_message);
this.content.scrollTop = top_visible_message.offsetTop - 30;
}
}
} else {
this.scrollDown();
}
},
getExtraMessageTemplateAttributes (attrs) {
/* Provides a hook for sending more attributes to the
* message template.
*
* Parameters:
* (Object) attrs: An object containing message attributes.
*/
if (attrs.is_spoiler) {
return {'label_show': __('Show spoiler')};
} else {
return {}
}
},
getExtraMessageClasses (attrs) {
2018-01-18 11:41:47 +01:00
if (_converse.show_message_load_animation) {
2018-01-16 16:55:38 +01:00
return 'onload ' + (attrs.delayed && 'delayed' || '');
2018-01-18 11:41:47 +01:00
} else {
return attrs.delayed && 'delayed' || '';
2018-01-16 16:55:38 +01:00
}
2017-02-15 21:30:32 +01:00
},
2018-02-06 09:55:45 +01:00
renderSpoilerMessage (msg, attrs) {
/* Render a "spoiler" message, as defined in XEP-0382
*
* Parameters:
* (HTMLElement) msg: The chat message DOM element
2018-02-06 09:55:45 +01:00
* (Object) attrs: An object containing the message attributes.
*/
const hint = msg.querySelector('.spoiler-hint');
hint.appendChild(document.createTextNode(attrs.spoiler_hint || ''));
2018-02-06 09:55:45 +01:00
},
renderMessage (attrs) {
2016-03-13 17:16:53 +01:00
/* Renders a chat message based on the passed in attributes.
2016-03-19 23:16:00 +01:00
*
* Parameters:
* (Object) attrs: An object containing the message attributes.
*
* Returns:
* The DOM element representing the message.
*/
let text = attrs.message,
2016-03-13 17:16:53 +01:00
fullname = this.model.get('fullname') || attrs.fullname,
template, username;
const match = text.match(/^\/(.*?)(?: (.*))?$/);
2016-03-13 17:16:53 +01:00
if ((match) && (match[1] === 'me')) {
text = text.replace(/^\/me/, '');
template = tpl_action;
2017-02-15 22:03:02 +01:00
if (attrs.sender === 'me') {
fullname = _converse.xmppstatus.get('fullname') || attrs.fullname;
2017-02-15 22:03:02 +01:00
username = _.isNil(fullname)? _converse.bare_jid: fullname;
} else {
username = attrs.fullname;
}
} else if (attrs.is_spoiler) {
template = tpl_spoiler_message;
2016-03-13 17:16:53 +01:00
} else {
template = tpl_message;
2016-03-13 17:16:53 +01:00
username = attrs.sender === 'me' && __('me') || fullname;
}
const msg_time = moment(attrs.time) || moment;
2018-01-02 21:44:46 +01:00
const msg = u.stringToElement(template(
_.extend(this.getExtraMessageTemplateAttributes(attrs), {
'msgid': attrs.msgid,
'sender': attrs.sender,
'time': msg_time.format(_converse.time_format),
'isodate': msg_time.format(),
'username': username,
2017-02-15 21:30:32 +01:00
'extra_classes': this.getExtraMessageClasses(attrs)
})
));
2018-01-18 11:41:47 +01:00
if (_converse.show_message_load_animation) {
window.setTimeout(
_.partial(u.removeClass, 'onload', msg), 2000);
2018-01-16 16:55:38 +01:00
}
2018-01-02 21:44:46 +01:00
const msg_content = msg.querySelector('.chat-msg-content');
msg_content.innerHTML = u.addEmoji(
_converse, emojione, u.addHyperlinks(xss.filterXSS(text, {'whiteList': {}}))
);
2018-02-06 09:55:45 +01:00
if (attrs.is_spoiler) {
this.renderSpoilerMessage(msg, attrs)
2018-02-06 09:55:45 +01:00
}
u.renderImageURLs(msg_content).then(this.scrollDown.bind(this));
2018-01-02 21:44:46 +01:00
return msg;
2016-03-13 17:16:53 +01:00
},
showHelpMessages (msgs, type, spinner) {
_.each(msgs, (msg) => {
this.content.insertAdjacentHTML(
'beforeend',
tpl_help_message({
'isodate': moment().format(),
'type': type||'info',
'message': xss.filterXSS(msg, {'whiteList': {'strong': []}})
})
);
});
2016-03-13 17:16:53 +01:00
if (spinner === true) {
2018-01-02 21:44:46 +01:00
this.addSpinner();
2016-03-13 17:16:53 +01:00
} else if (spinner === false) {
2018-01-02 21:25:30 +01:00
this.clearSpinner();
2016-03-13 17:16:53 +01:00
}
return this.scrollDown();
},
handleChatStateMessage (message) {
if (message.get('chat_state') === _converse.COMPOSING) {
if (message.get('sender') === 'me') {
this.showStatusNotification(__('Typing from another device'));
} else {
this.showStatusNotification(message.get('fullname')+' '+__('is typing'));
}
this.clear_status_timeout = window.setTimeout(
this.clearStatusNotification.bind(this),
30000
);
} else if (message.get('chat_state') === _converse.PAUSED) {
if (message.get('sender') === 'me') {
this.showStatusNotification(__('Stopped typing on the other device'));
} else {
this.showStatusNotification(message.get('fullname')+' '+__('has stopped typing'));
}
} else if (_.includes([_converse.INACTIVE, _converse.ACTIVE], message.get('chat_state'))) {
2018-01-02 21:44:46 +01:00
this.clearStatusNotification();
} else if (message.get('chat_state') === _converse.GONE) {
2016-03-13 17:16:53 +01:00
this.showStatusNotification(message.get('fullname')+' '+__('has gone away'));
}
return message;
2016-03-13 17:16:53 +01:00
},
shouldShowOnTextMessage () {
2018-01-02 21:44:46 +01:00
return !u.isVisible(this.el);
},
handleTextMessage (message) {
this.showMessage(_.clone(message.attributes));
if (u.isNewMessage(message)) {
if (message.get('sender') === 'me') {
// We remove the "scrolled" flag so that the chat area
// gets scrolled down. We always want to scroll down
// when the user writes a message as opposed to when a
// message is received.
this.model.set('scrolled', false);
} else if (this.model.get('scrolled', true)) {
this.showNewMessagesIndicator();
}
2016-03-13 17:16:53 +01:00
}
if (this.shouldShowOnTextMessage()) {
2016-03-13 17:16:53 +01:00
this.show();
} else {
this.scrollDown();
2016-03-13 17:16:53 +01:00
}
},
handleErrorMessage (message) {
const message_el = this.content.querySelector(`[data-msgid="${message.get('msgid')}"]`);
if (!_.isNull(message_el)) {
message_el.insertAdjacentHTML(
'afterend',
tpl_info({
'extra_classes': 'chat-error',
'message': message.get('message'),
'isodate': moment().format(),
'data': ''
}));
this.scrollDown();
}
},
onMessageAdded (message) {
2016-03-13 17:16:53 +01:00
/* Handler that gets called when a new message object is created.
2016-03-19 23:16:00 +01:00
*
* Parameters:
* (Object) message - The message Backbone object that was added.
*/
if (!_.isUndefined(this.clear_status_timeout)) {
2016-03-13 17:16:53 +01:00
window.clearTimeout(this.clear_status_timeout);
delete this.clear_status_timeout;
}
if (message.get('type') === 'error') {
this.handleErrorMessage(message);
2016-03-13 17:16:53 +01:00
} else {
if (message.get('chat_state')) {
this.handleChatStateMessage(message);
}
if (message.get('message')) {
this.handleTextMessage(message);
}
2016-03-13 17:16:53 +01:00
}
_converse.emit('messageAdded', {
'message': message,
'chatbox': this.model
});
2016-03-13 17:16:53 +01:00
},
createMessageStanza (message) {
const stanza = $msg({
'from': _converse.connection.jid,
'to': this.model.get('jid'),
'type': 'chat',
'id': message.get('msgid')
2016-03-13 17:16:53 +01:00
}).c('body').t(message.get('message')).up()
.c(_converse.ACTIVE, {'xmlns': Strophe.NS.CHATSTATES}).up();
if (message.get('is_spoiler')) {
if (message.get('spoiler_hint')) {
stanza.c('spoiler', {'xmlns': Strophe.NS.SPOILER }, message.get('spoiler_hint'));
} else {
stanza.c('spoiler', {'xmlns': Strophe.NS.SPOILER });
}
}
return stanza;
2016-03-13 17:16:53 +01:00
},
sendMessage (message) {
2016-03-13 17:16:53 +01:00
/* Responsible for sending off a text message.
2016-03-19 23:16:00 +01:00
*
* Parameters:
* (Message) message - The chat message
*/
2016-03-13 17:16:53 +01:00
// TODO: We might want to send to specfic resources.
// Especially in the OTR case.
const messageStanza = this.createMessageStanza(message);
_converse.connection.send(messageStanza);
if (_converse.forward_messages) {
2016-03-13 17:16:53 +01:00
// Forward the message, so that other connected resources are also aware of it.
_converse.connection.send(
$msg({ to: _converse.bare_jid, type: 'chat', id: message.get('msgid') })
.c('forwarded', {'xmlns': Strophe.NS.FORWARD})
.c('delay', {
'xmns': Strophe.NS.DELAY,
2018-01-22 12:40:43 +01:00
'stamp': moment().format()
}).up()
2016-03-13 17:16:53 +01:00
.cnode(messageStanza.tree())
);
}
},
2018-02-06 21:21:21 +01:00
parseMessageForCommands (text) {
const match = text.replace(/^\s*/, "").match(/^\/(.*)\s*$/);
if (match) {
if (match[1] === "clear") {
this.clearMessages();
return true;
}
else if (match[1] === "help") {
const msgs = [
`<strong>/clear</strong>: ${__('Remove messages')}`,
`<strong>/me</strong>: ${__('Write in the third person')}`,
`<strong>/help</strong>: ${__('Show this menu')}`
];
this.showHelpMessages(msgs);
return true;
}
}
},
onMessageSubmitted (text, spoiler_hint) {
2016-03-13 17:16:53 +01:00
/* This method gets called once the user has typed a message
2016-03-19 23:16:00 +01:00
* and then pressed enter in a chat box.
*
* Parameters:
* (String) text - The chat message text.
* (String) spoiler_hint - A hint in case the message
* text is a spoiler message. See XEP-0382
2016-03-19 23:16:00 +01:00
*/
if (!_converse.connection.authenticated) {
2016-03-13 17:16:53 +01:00
return this.showHelpMessages(
['Sorry, the connection has been lost, '+
'and your message could not be sent'],
'error'
);
}
2018-02-06 21:21:21 +01:00
if (this.parseMessageForCommands(text)) {
return;
2016-03-13 17:16:53 +01:00
}
const attrs = this.getOutgoingMessageAttributes(text, spoiler_hint)
2018-02-02 22:37:41 +01:00
const message = this.model.messages.create(attrs);
2016-03-13 17:16:53 +01:00
this.sendMessage(message);
},
2018-02-02 22:37:41 +01:00
getOutgoingMessageAttributes (text, spoiler_hint) {
2018-02-02 22:37:41 +01:00
/* Overridable method which returns the attributes to be
* passed to Backbone.Message's constructor.
*/
const fullname = _converse.xmppstatus.get('fullname'),
2018-02-04 19:31:02 +01:00
is_spoiler = this.message_form_view.model.get('sending_spoiler'),
attrs = {
'fullname': _.isEmpty(fullname) ? _converse.bare_jid : fullname,
'sender': 'me',
'time': moment().format(),
'message': emojione.shortnameToUnicode(text),
'is_spoiler': is_spoiler
};
if (is_spoiler) {
attrs.spoiler_hint = spoiler_hint;
2018-02-02 22:37:41 +01:00
}
return attrs;
2018-02-02 22:37:41 +01:00
},
2016-03-13 17:16:53 +01:00
sendChatState () {
2016-03-13 17:16:53 +01:00
/* Sends a message with the status of the user in this chat session
2016-03-19 23:16:00 +01:00
* as taken from the 'chat_state' attribute of the chat box.
* See XEP-0085 Chat State Notifications.
*/
_converse.connection.send(
2016-03-13 17:16:53 +01:00
$msg({'to':this.model.get('jid'), 'type': 'chat'})
.c(this.model.get('chat_state'), {'xmlns': Strophe.NS.CHATSTATES}).up()
.c('no-store', {'xmlns': Strophe.NS.HINTS}).up()
.c('no-permanent-store', {'xmlns': Strophe.NS.HINTS})
2016-03-13 17:16:53 +01:00
);
},
setChatState (state, no_save) {
2016-03-13 17:16:53 +01:00
/* Mutator for setting the chat state of this chat session.
2016-03-19 23:16:00 +01:00
* Handles clearing of any chat state notification timeouts and
* setting new ones if necessary.
* Timeouts are set when the state being set is COMPOSING or PAUSED.
* After the timeout, COMPOSING will become PAUSED and PAUSED will become INACTIVE.
* See XEP-0085 Chat State Notifications.
*
* Parameters:
* (string) state - The chat state (consts ACTIVE, COMPOSING, PAUSED, INACTIVE, GONE)
* (Boolean) no_save - Just do the cleanup or setup but don't actually save the state.
*/
if (!_.isUndefined(this.chat_state_timeout)) {
2016-03-13 17:16:53 +01:00
window.clearTimeout(this.chat_state_timeout);
delete this.chat_state_timeout;
}
if (state === _converse.COMPOSING) {
2016-03-13 17:16:53 +01:00
this.chat_state_timeout = window.setTimeout(
this.setChatState.bind(this),
_converse.TIMEOUTS.PAUSED,
_converse.PAUSED
);
} else if (state === _converse.PAUSED) {
2016-03-13 17:16:53 +01:00
this.chat_state_timeout = window.setTimeout(
this.setChatState.bind(this),
_converse.TIMEOUTS.INACTIVE,
_converse.INACTIVE
);
2016-03-13 17:16:53 +01:00
}
if (!no_save && this.model.get('chat_state') !== state) {
this.model.set('chat_state', state);
}
return this;
},
2017-07-15 15:58:11 +02:00
onFormSubmitted (ev) {
ev.preventDefault();
const textarea = this.el.querySelector('.chat-textarea'),
2017-07-15 15:58:11 +02:00
message = textarea.value;
let spoiler_hint;
2018-02-06 21:21:21 +01:00
if (this.message_form_view.model.get('sending_spoiler')) {
const hint_el = this.el.querySelector('form.sendXMPPMessage input.spoiler-hint');
spoiler_hint = hint_el.value;
hint_el.value = '';
}
textarea.value = '';
textarea.focus();
if (message !== '') {
this.onMessageSubmitted(message, spoiler_hint);
_converse.emit('messageSend', message);
}
this.setChatState(_converse.ACTIVE);
},
2017-07-15 15:58:11 +02:00
keyPressed (ev) {
/* Event handler for when a key is pressed in a chat box textarea.
*/
if (ev.keyCode === KEY.ENTER) {
this.onFormSubmitted(ev);
} else {
// Set chat state to composing if keyCode is not a forward-slash
// (which would imply an internal command and not a message).
this.setChatState(_converse.COMPOSING, ev.keyCode === KEY.FORWARD_SLASH);
}
},
clearMessages (ev) {
2016-03-13 17:16:53 +01:00
if (ev && ev.preventDefault) { ev.preventDefault(); }
const result = confirm(__("Are you sure you want to clear the messages from this chat box?"));
2016-03-13 17:16:53 +01:00
if (result === true) {
this.content.innerHTML = '';
2016-03-13 17:16:53 +01:00
this.model.messages.reset();
this.model.messages.browserStorage._clear();
}
return this;
},
insertIntoTextArea (value) {
2018-01-02 21:44:46 +01:00
const textbox_el = this.el.querySelector('.chat-textarea');
let existing = textbox_el.value;
if (existing && (existing[existing.length-1] !== ' ')) {
existing = existing + ' ';
}
2018-01-02 21:44:46 +01:00
textbox_el.value = existing+value+' ';
textbox_el.focus()
},
2017-07-15 11:55:07 +02:00
insertEmoji (ev) {
2016-03-13 17:16:53 +01:00
ev.stopPropagation();
const target = ev.target.nodeName === 'IMG' ? ev.target.parentElement : ev.target;
this.insertIntoTextArea(target.getAttribute('data-emoji'));
2016-03-13 17:16:53 +01:00
},
2017-07-15 11:55:07 +02:00
toggleEmojiMenu (ev) {
if (u.hasClass('insert-emoji', ev.target)) {
return;
}
2017-06-16 11:31:57 +02:00
if (!_.isUndefined(ev)) {
ev.stopPropagation();
if (ev.target.classList.contains('emoji-category-picker') ||
ev.target.classList.contains('emoji-skintone-picker') ||
ev.target.classList.contains('emoji-category')) {
return;
}
2017-06-16 11:31:57 +02:00
}
const elements = _.difference(
document.querySelectorAll('.toolbar-menu'),
[this.emoji_picker_view.el]
);
u.slideInAllElements(elements)
.then(_.partial(
u.slideToggleElement,
this.emoji_picker_view.el))
.then(this.focus.bind(this));
2016-03-13 17:16:53 +01:00
},
toggleCall (ev) {
2016-03-13 17:16:53 +01:00
ev.stopPropagation();
_converse.emit('callButtonClicked', {
connection: _converse.connection,
2016-03-13 17:16:53 +01:00
model: this.model
});
},
toggleSpoilerMessage (ev) {
if (ev && ev.preventDefault) {
ev.preventDefault();
}
const toggle_el = ev.target;
u.slideToggleElement(
toggle_el.parentElement.querySelector('.spoiler')
);
if (toggle_el.getAttribute("data-toggle-state") == "closed") {
toggle_el.textContent = __('Hide spoiler');
toggle_el.classList.remove("icon-eye");
toggle_el.classList.add("icon-eye-blocked");
toggle_el.setAttribute("data-toggle-state", "open");
} else {
toggle_el.textContent = __('Show spoiler');
toggle_el.classList.remove("icon-eye-blocked");
toggle_el.classList.add("icon-eye");
toggle_el.setAttribute("data-toggle-state", "closed");
}
},
onChatStatusChanged (item) {
const chat_status = item.get('chat_status');
let fullname = item.get('fullname');
2016-03-13 17:16:53 +01:00
fullname = _.isEmpty(fullname)? item.get('jid'): fullname;
2018-01-02 21:44:46 +01:00
if (u.isVisible(this.el)) {
2016-03-13 17:16:53 +01:00
if (chat_status === 'offline') {
this.showStatusNotification(fullname+' '+__('has gone offline'));
} else if (chat_status === 'away') {
this.showStatusNotification(fullname+' '+__('has gone away'));
} else if ((chat_status === 'dnd')) {
this.showStatusNotification(fullname+' '+__('is busy'));
} else if (chat_status === 'online') {
2018-01-02 21:44:46 +01:00
this.clearStatusNotification();
2016-03-13 17:16:53 +01:00
}
}
},
close (ev) {
2016-03-13 17:16:53 +01:00
if (ev && ev.preventDefault) { ev.preventDefault(); }
if (Backbone.history.getFragment() === "converse/chat?jid="+this.model.get('jid')) {
_converse.router.navigate('');
}
if (_converse.connection.connected) {
// Immediately sending the chat state, because the
// model is going to be destroyed afterwards.
this.setChatState(_converse.INACTIVE);
this.sendChatState();
2016-03-13 17:16:53 +01:00
}
2017-04-19 15:40:27 +02:00
try {
this.model.destroy();
} catch (e) {
_converse.log(e, Strophe.LogLevel.ERROR);
2017-04-19 15:40:27 +02:00
}
this.remove();
_converse.emit('chatBoxClosed', this);
2016-03-13 17:16:53 +01:00
return this;
},
renderEmojiPicker () {
2017-06-16 11:31:57 +02:00
var toggle = this.el.querySelector('.toggle-smiley');
2018-01-29 15:16:44 +01:00
if (!_.isNull(toggle)) {
toggle.innerHTML = '';
toggle.appendChild(this.emoji_picker_view.render().el);
}
2016-03-13 17:16:53 +01:00
},
focus () {
this.message_form_view.focus();
2016-03-13 17:16:53 +01:00
},
hide () {
this.el.classList.add('hidden');
2016-03-13 17:16:53 +01:00
return this;
},
afterShown (focus) {
if (u.isPersistableModel(this.model)) {
this.model.save();
}
this.setChatState(_converse.ACTIVE);
this.renderEmojiPicker();
this.scrollDown();
if (focus) {
this.focus();
}
},
2017-12-20 11:02:01 +01:00
_show (focus) {
/* Inner show method that gets debounced */
if (u.isVisible(this.el)) {
if (focus) { this.focus(); }
return;
}
u.fadeIn(this.el, _.bind(this.afterShown, this, focus));
},
2017-12-20 11:02:01 +01:00
show (focus) {
if (_.isUndefined(this.debouncedShow)) {
/* We wrap the method in a debouncer and set it on the
* instance, so that we have it debounced per instance.
* Debouncing it on the class-level is too broad.
*/
this.debouncedShow = _.debounce(this._show, 250, {'leading': true});
}
this.debouncedShow.apply(this, arguments);
return this;
},
showNewMessagesIndicator () {
u.showElement(this.el.querySelector('.new-msgs-indicator'));
},
hideNewMessagesIndicator () {
const new_msgs_indicator = this.el.querySelector('.new-msgs-indicator');
if (!_.isNull(new_msgs_indicator)) {
new_msgs_indicator.classList.add('hidden');
}
},
_markScrolled: function (ev) {
/* 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.
*/
if (ev && ev.preventDefault) { ev.preventDefault(); }
let scrolled = true;
const is_at_bottom =
2018-01-02 21:44:46 +01:00
(this.content.scrollTop + this.content.clientHeight) >=
this.content.scrollHeight - 62; // sigh...
if (is_at_bottom) {
scrolled = false;
this.onScrolledDown();
}
u.safeSave(this.model, {
'scrolled': scrolled,
'top_visible_message': null
});
},
viewUnreadMessages () {
this.model.save({
'scrolled': false,
'top_visible_message': null
});
2016-05-28 14:25:44 +02:00
this.scrollDown();
},
_scrollDown () {
2017-02-02 16:06:49 +01:00
/* Inner method that gets debounced */
2017-12-20 11:02:01 +01:00
if (_.isUndefined(this.content)) {
return;
}
if (u.isVisible(this.content) && !this.model.get('scrolled')) {
this.content.scrollTop = this.content.scrollHeight;
2016-03-13 17:16:53 +01:00
}
2017-02-02 16:06:49 +01:00
},
onScrolledDown() {
this.hideNewMessagesIndicator();
if (_converse.windowState !== 'hidden') {
this.model.clearUnreadMsgCounter();
}
_converse.emit('chatBoxScrolledDown', {'chatbox': this.model});
},
onWindowStateChanged (state) {
if (this.model.get('num_unread', 0) && !this.model.newMessageWillBeHidden()) {
this.model.clearUnreadMsgCounter();
}
2016-03-13 17:16:53 +01:00
}
});
}
});
return converse;
2016-03-13 17:16:53 +01:00
}));