2018-04-16 18:08:00 +02:00
|
|
|
// Converse.js
|
2016-03-13 17:16:53 +01:00
|
|
|
// http://conversejs.org
|
|
|
|
//
|
2018-04-16 18:08:00 +02:00
|
|
|
// Copyright (c) 2012-2018, the Converse.js developers
|
2016-03-13 17:16:53 +01:00
|
|
|
// Licensed under the Mozilla Public License (MPLv2)
|
|
|
|
|
|
|
|
(function (root, factory) {
|
2017-02-13 15:37:17 +01:00
|
|
|
define([
|
2017-02-14 15:08:39 +01:00
|
|
|
"converse-core",
|
2018-03-09 11:46:50 +01:00
|
|
|
"bootstrap",
|
2017-06-16 11:31:57 +02:00
|
|
|
"emojione",
|
2017-07-15 17:33:25 +02:00
|
|
|
"xss",
|
2017-12-24 16:46:49 +01:00
|
|
|
"tpl!action",
|
2016-09-23 10:54:55 +02:00
|
|
|
"tpl!chatbox",
|
2017-11-17 12:50:16 +01:00
|
|
|
"tpl!chatbox_head",
|
2018-02-04 18:33:39 +01:00
|
|
|
"tpl!chatbox_message_form",
|
2018-04-06 13:56:14 +02:00
|
|
|
"tpl!csn",
|
2017-06-16 11:31:57 +02:00
|
|
|
"tpl!emojis",
|
2018-04-06 13:56:14 +02:00
|
|
|
"tpl!error_message",
|
2017-04-21 18:35:34 +02:00
|
|
|
"tpl!help_message",
|
2017-12-24 16:46:49 +01:00
|
|
|
"tpl!info",
|
|
|
|
"tpl!new_day",
|
2018-04-16 21:58:11 +02:00
|
|
|
"tpl!toolbar_fileupload",
|
2017-12-24 16:46:49 +01:00
|
|
|
"tpl!spinner",
|
2018-02-08 17:06:53 +01:00
|
|
|
"tpl!spoiler_button",
|
2018-04-06 13:56:14 +02:00
|
|
|
"tpl!status_message",
|
2018-03-09 14:49:56 +01:00
|
|
|
"tpl!toolbar",
|
2018-04-16 18:08:00 +02:00
|
|
|
"converse-chatboxes",
|
|
|
|
"converse-message-view"
|
2016-09-23 10:54:55 +02:00
|
|
|
], factory);
|
|
|
|
}(this, function (
|
2016-12-20 11:42:20 +01:00
|
|
|
converse,
|
2018-03-09 11:46:50 +01:00
|
|
|
bootstrap,
|
2017-06-16 11:31:57 +02:00
|
|
|
emojione,
|
2017-07-15 17:33:25 +02:00
|
|
|
xss,
|
2017-12-24 16:46:49 +01:00
|
|
|
tpl_action,
|
2016-09-23 10:54:55 +02:00
|
|
|
tpl_chatbox,
|
2017-11-17 12:50:16 +01:00
|
|
|
tpl_chatbox_head,
|
2018-02-04 18:33:39 +01:00
|
|
|
tpl_chatbox_message_form,
|
2018-04-06 13:56:14 +02:00
|
|
|
tpl_csn,
|
2017-06-16 11:31:57 +02:00
|
|
|
tpl_emojis,
|
2018-04-06 13:56:14 +02:00
|
|
|
tpl_error_message,
|
2017-04-21 18:35:34 +02:00
|
|
|
tpl_help_message,
|
2017-12-24 16:46:49 +01:00
|
|
|
tpl_info,
|
|
|
|
tpl_new_day,
|
2018-04-16 21:58:11 +02:00
|
|
|
tpl_toolbar_fileupload,
|
2017-12-24 16:46:49 +01:00
|
|
|
tpl_spinner,
|
2018-02-08 17:06:53 +01:00
|
|
|
tpl_spoiler_button,
|
2018-04-06 13:56:14 +02:00
|
|
|
tpl_status_message,
|
2018-04-16 18:08:00 +02:00
|
|
|
tpl_toolbar
|
2016-09-23 10:54:55 +02:00
|
|
|
) {
|
2016-03-13 17:16:53 +01:00
|
|
|
"use strict";
|
2018-02-08 17:06:53 +01:00
|
|
|
const { $msg, Backbone, Promise, Strophe, _, b64_sha1, f, sizzle, moment } = converse.env;
|
2017-12-19 21:26:09 +01:00
|
|
|
const u = converse.env.utils;
|
2017-06-12 20:30:58 +02:00
|
|
|
const KEY = {
|
2016-03-13 17:16:53 +01:00
|
|
|
ENTER: 13,
|
|
|
|
FORWARD_SLASH: 47
|
|
|
|
};
|
|
|
|
|
2016-12-20 11:42:20 +01:00
|
|
|
converse.plugins.add('converse-chatview', {
|
2018-01-10 14:26:35 +01:00
|
|
|
/* 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
|
2018-01-10 14:26:35 +01:00
|
|
|
* this plugin.
|
2018-01-03 17:08:30 +01:00
|
|
|
*
|
|
|
|
* If the setting "strict_plugin_dependencies" is set to true,
|
2018-01-10 14:26:35 +01:00
|
|
|
* 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.
|
|
|
|
*/
|
2018-04-16 18:08:00 +02:00
|
|
|
dependencies: ["converse-chatboxes", "converse-disco", "converse-message-view"],
|
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
|
|
|
//
|
2016-03-13 17:16:53 +01:00
|
|
|
ChatBoxViews: {
|
2017-07-10 17:46:22 +02:00
|
|
|
onChatBoxAdded (item) {
|
|
|
|
const { _converse } = this.__super__;
|
2017-06-12 20:30:58 +02:00
|
|
|
let view = this.get(item.get('id'));
|
2016-03-21 10:57:38 +01:00
|
|
|
if (!view) {
|
2016-12-20 10:30:20 +01:00
|
|
|
view = new _converse.ChatBoxView({model: item});
|
2016-03-13 17:16:53 +01:00
|
|
|
this.add(item.get('id'), view);
|
2016-03-19 22:56:55 +01:00
|
|
|
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
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2017-07-10 17:46:22 +02: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.
|
|
|
|
*/
|
2017-07-10 17:46:22 +02:00
|
|
|
const { _converse } = this,
|
|
|
|
{ __ } = _converse;
|
2016-12-20 11:42:20 +01:00
|
|
|
|
2017-07-05 11:03:13 +02:00
|
|
|
_converse.api.settings.update({
|
2018-03-25 13:12:56 +02:00
|
|
|
'use_emojione': false,
|
2017-07-16 12:27:21 +02:00
|
|
|
'emojione_image_path': emojione.imagePathPNG,
|
|
|
|
'chatview_avatar_height': 32,
|
|
|
|
'chatview_avatar_width': 32,
|
|
|
|
'show_toolbar': true,
|
|
|
|
'time_format': 'HH:mm',
|
|
|
|
'visible_toolbar_buttons': {
|
2016-10-26 12:50:00 +02:00
|
|
|
'call': false,
|
2018-02-07 11:00:35 +01:00
|
|
|
'clear': true,
|
|
|
|
'emoji': true,
|
|
|
|
'spoiler': true
|
2016-10-26 12:50:00 +02:00
|
|
|
},
|
2016-03-13 17:16:53 +01:00
|
|
|
});
|
2017-07-16 12:27:21 +02:00
|
|
|
emojione.imagePathPNG = _converse.emojione_image_path;
|
2017-07-16 12:50:59 +02:00
|
|
|
emojione.ascii = true;
|
2016-03-13 17:16:53 +01:00
|
|
|
|
2017-06-12 20:30:58 +02:00
|
|
|
function onWindowStateChanged (data) {
|
2017-05-03 09:06:28 +02:00
|
|
|
_converse.chatboxviews.each(function (chatboxview) {
|
2017-06-12 20:30:58 +02:00
|
|
|
chatboxview.onWindowStateChanged(data.state);
|
|
|
|
});
|
|
|
|
}
|
2017-05-03 09:06:28 +02:00
|
|
|
_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: {
|
2017-07-16 11:06:34 +02:00
|
|
|
'current_category': 'people',
|
2017-07-16 11:36:31 +02:00
|
|
|
'current_skintone': '',
|
|
|
|
'scroll_position': 0
|
2017-07-16 14:12:17 +02:00
|
|
|
},
|
|
|
|
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
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2018-03-09 14:49:56 +01:00
|
|
|
_converse.EmojiPickerView = Backbone.VDOMView.extend({
|
2018-03-09 11:46:50 +01:00
|
|
|
className: 'emoji-picker-container',
|
2017-06-16 11:31:57 +02:00
|
|
|
events: {
|
2017-07-16 13:41:01 +02:00
|
|
|
'click .emoji-category-picker li.emoji-category': 'chooseCategory',
|
2017-07-16 14:52:19 +02:00
|
|
|
'click .emoji-skintone-picker li.emoji-skintone': 'chooseSkinTone'
|
2017-06-16 11:31:57 +02:00
|
|
|
},
|
|
|
|
|
2017-07-16 11:36:31 +02:00
|
|
|
initialize () {
|
|
|
|
this.model.on('change:current_skintone', this.render, this);
|
|
|
|
this.model.on('change:current_category', this.render, this);
|
2017-06-16 11:31:57 +02:00
|
|
|
},
|
|
|
|
|
2018-03-09 14:49:56 +01:00
|
|
|
toHTML () {
|
|
|
|
return tpl_emojis(
|
2017-06-16 11:31:57 +02:00
|
|
|
_.extend(
|
|
|
|
this.model.toJSON(), {
|
2017-07-16 11:06:34 +02:00
|
|
|
'transform': _converse.use_emojione ? emojione.shortnameToImage : emojione.shortnameToUnicode,
|
2017-12-19 21:26:09 +01:00
|
|
|
'emojis_by_category': u.getEmojisByCategory(_converse, emojione),
|
|
|
|
'toned_emojis': u.getTonedEmojis(_converse),
|
2017-07-16 11:06:34 +02:00
|
|
|
'skintones': ['tone1', 'tone2', 'tone3', 'tone4', 'tone5'],
|
2017-07-16 12:14:56 +02:00
|
|
|
'shouldBeHidden': this.shouldBeHidden
|
2017-06-16 11:31:57 +02:00
|
|
|
}
|
|
|
|
));
|
|
|
|
},
|
|
|
|
|
2017-07-16 12:14:56 +02:00
|
|
|
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;
|
|
|
|
},
|
|
|
|
|
2017-07-16 11:36:31 +02:00
|
|
|
chooseSkinTone (ev) {
|
2017-07-16 11:06:34 +02:00
|
|
|
ev.preventDefault();
|
|
|
|
ev.stopPropagation();
|
2017-07-16 13:41:01 +02:00
|
|
|
const target = ev.target.nodeName === 'IMG' ?
|
|
|
|
ev.target.parentElement : ev.target;
|
|
|
|
const skintone = target.getAttribute("data-skintone").trim();
|
2017-07-16 12:14:56 +02:00
|
|
|
if (this.model.get('current_skintone') === skintone) {
|
2017-07-16 14:12:17 +02:00
|
|
|
this.model.save({'current_skintone': ''});
|
2017-07-16 12:14:56 +02:00
|
|
|
} else {
|
2017-07-16 14:12:17 +02:00
|
|
|
this.model.save({'current_skintone': skintone});
|
2017-07-16 12:14:56 +02:00
|
|
|
}
|
2017-07-16 11:06:34 +02:00
|
|
|
},
|
|
|
|
|
2017-07-16 11:36:31 +02:00
|
|
|
chooseCategory (ev) {
|
2017-06-16 11:31:57 +02:00
|
|
|
ev.preventDefault();
|
|
|
|
ev.stopPropagation();
|
2017-07-16 13:41:01 +02:00
|
|
|
const target = ev.target.nodeName === 'IMG' ?
|
|
|
|
ev.target.parentElement : ev.target;
|
|
|
|
const category = target.getAttribute("data-category").trim();
|
2017-07-16 14:12:17 +02:00
|
|
|
this.model.save({
|
2017-07-16 11:36:31 +02:00
|
|
|
'current_category': category,
|
|
|
|
'scroll_position': 0
|
|
|
|
});
|
2017-06-16 11:31:57 +02:00
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2018-01-03 20:02:05 +01:00
|
|
|
_converse.ChatBoxHeading = Backbone.NativeView.extend({
|
2017-11-17 12:50:16 +01:00
|
|
|
|
|
|
|
initialize () {
|
2017-12-02 18:07:31 +01:00
|
|
|
this.model.on('change:image', this.render, this);
|
2017-11-17 12:50:16 +01:00
|
|
|
this.model.on('change:status', this.onStatusMessageChanged, this);
|
|
|
|
this.model.on('change:fullname', this.render, this);
|
|
|
|
},
|
|
|
|
|
2017-11-17 13:19:21 +01:00
|
|
|
render () {
|
|
|
|
this.el.innerHTML = tpl_chatbox_head(
|
2017-11-17 12:50:16 +01:00
|
|
|
_.extend(this.model.toJSON(), {
|
2018-03-04 07:09:49 +01:00
|
|
|
'_converse': _converse,
|
2017-11-17 12:50:16 +01:00
|
|
|
'avatar_width': _converse.chatview_avatar_width,
|
|
|
|
'avatar_height': _converse.chatview_avatar_height,
|
|
|
|
'info_close': __('Close this chat box'),
|
|
|
|
})
|
|
|
|
);
|
2017-11-17 13:19:21 +01:00
|
|
|
return this;
|
2017-11-17 12:50:16 +01:00
|
|
|
},
|
|
|
|
|
|
|
|
onStatusMessageChanged (item) {
|
|
|
|
this.render();
|
|
|
|
_converse.emit('contactStatusMessageChanged', {
|
|
|
|
'contact': item.attributes,
|
|
|
|
'message': item.get('status')
|
|
|
|
});
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2018-04-16 18:08:00 +02:00
|
|
|
|
2018-01-03 20:02:05 +01:00
|
|
|
_converse.ChatBoxView = Backbone.NativeView.extend({
|
2016-03-13 17:16:53 +01:00
|
|
|
length: 200,
|
2016-11-22 17:42:58 +01:00
|
|
|
className: 'chatbox hidden',
|
2016-10-26 12:50:00 +02:00
|
|
|
is_chatroom: false, // Leaky abstraction from MUC
|
2016-03-13 17:16:53 +01:00
|
|
|
|
|
|
|
events: {
|
2018-04-16 21:58:11 +02:00
|
|
|
'change input.fileupload': 'onFileSelection',
|
2016-03-13 17:16:53 +01:00
|
|
|
'click .close-chatbox-button': 'close',
|
2018-02-04 18:55:38 +01:00
|
|
|
'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',
|
2018-02-04 18:55:38 +01:00
|
|
|
'click .toggle-clear': 'clearMessages',
|
2018-04-16 21:58:11 +02:00
|
|
|
'click .toggle-compose-spoiler': 'toggleComposeSpoilerMessage',
|
2018-02-04 18:55:38 +01:00
|
|
|
'click .toggle-smiley ul.emoji-picker li': 'insertEmoji',
|
|
|
|
'click .toggle-smiley': 'toggleEmojiMenu',
|
2018-02-06 17:53:56 +01:00
|
|
|
'click .toggle-spoiler': 'toggleSpoilerMessage',
|
2018-04-16 21:58:11 +02:00
|
|
|
'click .upload-file': 'toggleFileUpload',
|
2018-02-04 18:55:38 +01:00
|
|
|
'keypress .chat-textarea': 'keyPressed'
|
2016-03-13 17:16:53 +01:00
|
|
|
},
|
|
|
|
|
2017-07-10 17:46:22 +02:00
|
|
|
initialize () {
|
2018-01-02 21:44:46 +01:00
|
|
|
this.scrollDown = _.debounce(this._scrollDown, 250);
|
2017-12-25 12:58:30 +01:00
|
|
|
this.markScrolled = _.debounce(this._markScrolled, 100);
|
2017-07-16 14:12:17 +02:00
|
|
|
this.createEmojiPicker();
|
2016-03-13 17:16:53 +01:00
|
|
|
this.model.messages.on('add', this.onMessageAdded, this);
|
2018-04-16 18:08:00 +02:00
|
|
|
this.model.messages.on('rendered', this.scrollDown, this);
|
|
|
|
|
2016-03-13 17:16:53 +01:00
|
|
|
this.model.on('show', this.show, this);
|
2017-11-17 12:50:16 +01:00
|
|
|
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_status', this.onChatStatusChanged, this);
|
|
|
|
this.model.on('showHelpMessages', this.showHelpMessages, this);
|
2018-02-04 18:33:39 +01:00
|
|
|
this.render();
|
|
|
|
this.fetchMessages();
|
2017-11-17 12:50:16 +01:00
|
|
|
_converse.emit('chatBoxOpened', this);
|
2016-12-20 10:30:20 +01:00
|
|
|
_converse.emit('chatBoxInitialized', this);
|
2016-03-13 17:16:53 +01:00
|
|
|
},
|
|
|
|
|
2017-07-10 17:46:22 +02:00
|
|
|
render () {
|
2018-03-11 13:56:53 +01:00
|
|
|
// XXX: Is this still needed?
|
2017-11-17 12:50:16 +01:00
|
|
|
this.el.setAttribute('id', this.model.get('box_id'));
|
|
|
|
this.el.innerHTML = tpl_chatbox(
|
|
|
|
_.extend(this.model.toJSON(), {
|
2017-11-17 14:41:54 +01:00
|
|
|
unread_msgs: __('You have unread messages')
|
2017-11-17 12:50:16 +01:00
|
|
|
}
|
2017-11-17 14:41:54 +01:00
|
|
|
));
|
2017-12-02 14:57:10 +01:00
|
|
|
this.content = this.el.querySelector('.chat-content');
|
2018-02-07 11:00:35 +01:00
|
|
|
this.renderMessageForm();
|
|
|
|
this.insertHeading();
|
2017-11-17 12:50:16 +01:00
|
|
|
return this;
|
|
|
|
},
|
|
|
|
|
2018-02-07 11:00:35 +01:00
|
|
|
renderToolbar (toolbar, options) {
|
|
|
|
if (!_converse.show_toolbar) {
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
toolbar = toolbar || tpl_toolbar;
|
|
|
|
options = _.assign(
|
|
|
|
this.model.toJSON(),
|
|
|
|
this.getToolbarOptions(options || {})
|
2018-02-04 19:31:02 +01:00
|
|
|
);
|
2018-02-07 11:00:35 +01:00
|
|
|
this.el.querySelector('.chat-toolbar').innerHTML = toolbar(options);
|
2018-02-08 17:06:53 +01:00
|
|
|
this.addSpoilerButton(options);
|
2018-04-16 21:58:11 +02:00
|
|
|
this.addFileUploadButton();
|
2018-02-07 11:00:35 +01:00
|
|
|
this.insertEmojiPicker();
|
|
|
|
return this;
|
|
|
|
},
|
|
|
|
|
|
|
|
renderMessageForm () {
|
|
|
|
let placeholder;
|
|
|
|
if (this.model.get('composing_spoiler')) {
|
2018-02-07 14:35:53 +01:00
|
|
|
placeholder = __('Hidden message');
|
2018-02-07 11:00:35 +01:00
|
|
|
} else {
|
|
|
|
placeholder = __('Personal message');
|
|
|
|
}
|
|
|
|
const form_container = this.el.querySelector('.message-form-container');
|
|
|
|
form_container.innerHTML = tpl_chatbox_message_form(
|
|
|
|
_.extend(this.model.toJSON(), {
|
|
|
|
'hint_value': _.get(this.el.querySelector('.spoiler-hint'), 'value'),
|
|
|
|
'label_personal_message': placeholder,
|
|
|
|
'label_send': __('Send'),
|
|
|
|
'label_spoiler_hint': __('Optional hint'),
|
|
|
|
'message_value': _.get(this.el.querySelector('.chat-textarea'), 'value'),
|
|
|
|
'show_send_button': _converse.show_send_button,
|
2018-02-07 13:29:54 +01:00
|
|
|
'show_toolbar': _converse.show_toolbar,
|
|
|
|
'unread_msgs': __('You have unread messages')
|
2018-02-07 11:00:35 +01:00
|
|
|
}));
|
|
|
|
this.renderToolbar();
|
2018-02-02 20:53:10 +01:00
|
|
|
},
|
|
|
|
|
2018-04-16 21:58:11 +02:00
|
|
|
toggleFileUpload (ev) {
|
|
|
|
this.el.querySelector('input.fileupload').click();
|
|
|
|
},
|
|
|
|
|
|
|
|
onFileSelection (evt) {
|
|
|
|
this.model.sendFiles(evt.target.files);
|
|
|
|
},
|
|
|
|
|
|
|
|
addFileUploadButton (options) {
|
|
|
|
_converse.api.disco.supports(Strophe.NS.HTTPUPLOAD, _converse.domain).then((result) => {
|
2018-04-22 17:47:34 +02:00
|
|
|
if (result.length) {
|
|
|
|
this.el.querySelector('.chat-toolbar').insertAdjacentHTML(
|
|
|
|
'beforeend',
|
|
|
|
tpl_toolbar_fileupload({'tooltip_upload_file': __('Choose a file to send')}));
|
|
|
|
}
|
2018-04-16 21:58:11 +02:00
|
|
|
});
|
|
|
|
},
|
|
|
|
|
2018-02-08 17:06:53 +01:00
|
|
|
addSpoilerButton (options) {
|
|
|
|
/* Asynchronously adds a button for writing spoiler
|
|
|
|
* messages, based on whether the contact's client supports
|
|
|
|
* it.
|
|
|
|
*/
|
|
|
|
if (!options.show_spoiler_button || this.model.get('type') === 'chatroom') {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
const contact_jid = this.model.get('jid');
|
|
|
|
const resources = this.model.get('resources');
|
|
|
|
if (_.isEmpty(resources)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
Promise.all(_.map(_.keys(resources), (resource) =>
|
|
|
|
_converse.api.disco.supports(Strophe.NS.SPOILER, `${contact_jid}/${resource}`)
|
|
|
|
)).then((results) => {
|
2018-03-29 16:12:19 +02:00
|
|
|
if (results.length) {
|
2018-02-08 17:06:53 +01:00
|
|
|
const html = tpl_spoiler_button(this.model.toJSON());
|
|
|
|
if (_converse.visible_toolbar_buttons.emoji) {
|
|
|
|
this.el.querySelector('.toggle-smiley').insertAdjacentHTML('afterEnd', html);
|
|
|
|
} else {
|
|
|
|
this.el.querySelector('.chat-toolbar').insertAdjacentHTML('afterBegin', html);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
},
|
|
|
|
|
2017-11-17 12:50:16 +01:00
|
|
|
insertHeading () {
|
2017-11-17 14:41:54 +01:00
|
|
|
this.heading = new _converse.ChatBoxHeading({'model': this.model});
|
|
|
|
this.heading.render();
|
|
|
|
this.heading.chatview = this;
|
|
|
|
|
2017-11-17 12:50:16 +01:00
|
|
|
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
|
|
|
},
|
|
|
|
|
2018-02-07 11:00:35 +01:00
|
|
|
getToolbarOptions (options) {
|
|
|
|
let label_toggle_spoiler;
|
|
|
|
if (this.model.get('composing_spoiler')) {
|
|
|
|
label_toggle_spoiler = __('Click to write as a normal (non-spoiler) message');
|
|
|
|
} else {
|
|
|
|
label_toggle_spoiler = __('Click to write your message as a spoiler');
|
2017-07-16 14:12:17 +02:00
|
|
|
}
|
2018-02-07 11:00:35 +01:00
|
|
|
return _.extend(options || {}, {
|
|
|
|
'label_clear': __('Clear all messages'),
|
2018-04-14 21:13:02 +02:00
|
|
|
'tooltip_insert_smiley': __('Insert emojis'),
|
|
|
|
'tooltip_start_call': __('Start a call'),
|
2018-02-07 11:00:35 +01:00
|
|
|
'label_toggle_spoiler': label_toggle_spoiler,
|
|
|
|
'show_call_button': _converse.visible_toolbar_buttons.call,
|
|
|
|
'show_spoiler_button': _converse.visible_toolbar_buttons.spoiler,
|
|
|
|
'use_emoji': _converse.visible_toolbar_buttons.emoji,
|
2017-07-16 14:12:17 +02:00
|
|
|
});
|
|
|
|
},
|
|
|
|
|
2017-07-10 17:46:22 +02:00
|
|
|
afterMessagesFetched () {
|
2017-02-24 22:18:41 +01:00
|
|
|
this.insertIntoDOM();
|
2017-02-25 22:17:14 +01:00
|
|
|
this.scrollDown();
|
2017-12-24 16:46:49 +01:00
|
|
|
this.content.addEventListener('scroll', this.markScrolled.bind(this));
|
2017-07-21 15:01:35 +02:00
|
|
|
_converse.emit('afterMessagesFetched', this);
|
2016-03-13 17:16:53 +01:00
|
|
|
},
|
|
|
|
|
2017-07-10 17:46:22 +02:00
|
|
|
fetchMessages () {
|
2016-03-13 17:16:53 +01:00
|
|
|
this.model.messages.fetch({
|
2017-05-15 18:04:03 +02:00
|
|
|
'add': true,
|
2017-02-24 22:18:41 +01:00
|
|
|
'success': this.afterMessagesFetched.bind(this),
|
|
|
|
'error': this.afterMessagesFetched.bind(this),
|
2016-03-13 17:16:53 +01:00
|
|
|
});
|
|
|
|
return this;
|
|
|
|
},
|
|
|
|
|
2017-07-10 17:46:22 +02:00
|
|
|
insertIntoDOM () {
|
2017-08-29 11:08:37 +02:00
|
|
|
/* 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
|
|
|
*/
|
2018-02-15 15:50:55 +01:00
|
|
|
_converse.chatboxviews.insertRowColumn(this.el);
|
2016-03-13 17:16:53 +01:00
|
|
|
return this;
|
|
|
|
},
|
|
|
|
|
2018-04-06 13:56:14 +02:00
|
|
|
showChatEvent (message, data='') {
|
|
|
|
const isodate = moment().format();
|
2017-12-24 16:46:49 +01:00
|
|
|
this.content.insertAdjacentHTML(
|
|
|
|
'beforeend',
|
|
|
|
tpl_info({
|
2018-04-06 13:56:14 +02:00
|
|
|
'extra_classes': 'chat-event',
|
2017-12-24 16:46:49 +01:00
|
|
|
'message': message,
|
2018-04-06 13:56:14 +02:00
|
|
|
'isodate': isodate,
|
|
|
|
'data': data
|
2017-12-24 16:46:49 +01:00
|
|
|
}));
|
2018-04-08 19:44:53 +02:00
|
|
|
this.insertDayIndicator(this.content.lastElementChild);
|
2016-05-25 15:14:12 +02:00
|
|
|
this.scrollDown();
|
2018-04-06 13:56:14 +02:00
|
|
|
return isodate;
|
|
|
|
},
|
|
|
|
|
|
|
|
showErrorMessage (message) {
|
|
|
|
this.content.insertAdjacentHTML(
|
|
|
|
'beforeend',
|
|
|
|
tpl_error_message({'message': message, 'isodate': moment().format() })
|
|
|
|
);
|
|
|
|
this.scrollDown();
|
2016-03-13 17:16:53 +01:00
|
|
|
},
|
|
|
|
|
2017-12-02 14:57:10 +01:00
|
|
|
addSpinner (append=false) {
|
2017-04-04 09:56:18 +02:00
|
|
|
if (_.isNull(this.el.querySelector('.spinner'))) {
|
2017-12-02 14:57:10 +01:00
|
|
|
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
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2017-07-10 17:46:22 +02:00
|
|
|
clearSpinner () {
|
2017-12-02 14:57:10 +01:00
|
|
|
_.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
|
|
|
*
|
2018-01-03 13:55:57 +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
|
|
|
*/
|
2018-04-06 13:56:14 +02:00
|
|
|
const prev_msg_el = u.getPreviousElement(next_msg_el, ".message:not(.chat-state-notification)"),
|
2018-01-03 13:55:57 +01:00
|
|
|
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.
|
|
|
|
*/
|
2018-04-06 13:56:14 +02:00
|
|
|
const first_msg = u.getFirstChildElement(this.content, '.message:not(.chat-state-notification)'),
|
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;
|
|
|
|
}
|
2018-04-06 13:56:14 +02:00
|
|
|
const last_msg = u.getLastChildElement(this.content, '.message:not(.chat-state-notification)'),
|
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
|
|
|
}
|
2018-04-06 13:56:14 +02:00
|
|
|
/* XXX: We avoid .chat-state-notification messages, since they are
|
2018-01-10 13:45:12 +01:00
|
|
|
* 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.
|
2018-01-09 13:33:57 +01:00
|
|
|
*/
|
2017-12-24 17:47:02 +01:00
|
|
|
const msg_dates = _.invokeMap(
|
2018-04-06 13:56:14 +02:00
|
|
|
sizzle('.message:not(.chat-state-notification)', this.content),
|
2018-01-02 21:25:30 +01:00
|
|
|
Element.prototype.getAttribute, 'data-isodate'
|
2017-12-24 17:47:02 +01:00
|
|
|
)
|
2017-12-25 12:58:30 +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];
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2018-01-16 15:38:08 +01:00
|
|
|
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();
|
|
|
|
}
|
2017-01-26 15:49:02 +01:00
|
|
|
},
|
|
|
|
|
2017-07-10 17:46:22 +02:00
|
|
|
showHelpMessages (msgs, type, spinner) {
|
2017-06-12 20:30:58 +02:00
|
|
|
_.each(msgs, (msg) => {
|
2017-12-06 17:01:17 +01:00
|
|
|
this.content.insertAdjacentHTML(
|
|
|
|
'beforeend',
|
|
|
|
tpl_help_message({
|
2018-01-15 20:32:24 +01:00
|
|
|
'isodate': moment().format(),
|
2018-04-14 21:13:02 +02:00
|
|
|
'type': type,
|
2017-12-06 17:01:17 +01:00
|
|
|
'message': xss.filterXSS(msg, {'whiteList': {'strong': []}})
|
|
|
|
})
|
|
|
|
);
|
2017-06-12 20:30:58 +02:00
|
|
|
});
|
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();
|
|
|
|
},
|
|
|
|
|
2018-04-06 13:56:14 +02:00
|
|
|
clearChatStateNotification (from, isodate) {
|
|
|
|
if (isodate) {
|
|
|
|
_.each(
|
|
|
|
sizzle(`.chat-state-notification[data-csn="${from}"][data-isodate="${isodate}"]`, this.content),
|
|
|
|
u.removeElement
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
_.each(sizzle(`.chat-state-notification[data-csn="${from}"]`, this.content), u.removeElement);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
showChatStateNotification (message) {
|
|
|
|
/* Support for XEP-0085, Chat State Notifications */
|
|
|
|
let text;
|
2018-04-17 15:17:39 +02:00
|
|
|
const from = message.get('from'),
|
|
|
|
username = message.get('fullname') || from,
|
|
|
|
data = `data-csn=${from}`;
|
2018-04-06 13:56:14 +02:00
|
|
|
this.clearChatStateNotification(from);
|
|
|
|
|
2016-12-20 10:30:20 +01:00
|
|
|
if (message.get('chat_state') === _converse.COMPOSING) {
|
2018-04-11 18:46:50 +02:00
|
|
|
if (message.get('sender') === 'me') {
|
|
|
|
text = __('Typing from another device');
|
|
|
|
} else {
|
2018-04-17 15:17:39 +02:00
|
|
|
text = username +' '+__('is typing');
|
2018-04-11 18:46:50 +02:00
|
|
|
}
|
2016-12-20 10:30:20 +01:00
|
|
|
} else if (message.get('chat_state') === _converse.PAUSED) {
|
2018-04-11 18:46:50 +02:00
|
|
|
if (message.get('sender') === 'me') {
|
|
|
|
text = __('Stopped typing on the other device');
|
|
|
|
} else {
|
2018-04-17 15:17:39 +02:00
|
|
|
text = username +' '+__('has stopped typing');
|
2018-04-11 18:46:50 +02:00
|
|
|
}
|
2016-12-20 10:30:20 +01:00
|
|
|
} else if (message.get('chat_state') === _converse.GONE) {
|
2018-04-17 15:17:39 +02:00
|
|
|
text = username +' '+__('has gone away');
|
2018-04-06 13:56:14 +02:00
|
|
|
} else {
|
|
|
|
return;
|
2016-03-13 17:16:53 +01:00
|
|
|
}
|
2018-04-06 13:56:14 +02:00
|
|
|
const isodate = moment().format();
|
|
|
|
this.content.insertAdjacentHTML(
|
|
|
|
'beforeend',
|
|
|
|
tpl_csn({
|
|
|
|
'message': text,
|
|
|
|
'from': from,
|
|
|
|
'isodate': isodate
|
|
|
|
}));
|
|
|
|
this.scrollDown();
|
|
|
|
|
|
|
|
this.clear_status_timeout = window.setTimeout(
|
|
|
|
this.clearChatStateNotification.bind(this, from, isodate),
|
|
|
|
30000
|
|
|
|
);
|
2018-01-09 13:33:57 +01:00
|
|
|
return message;
|
2016-03-13 17:16:53 +01:00
|
|
|
},
|
|
|
|
|
2018-03-13 19:11:49 +01:00
|
|
|
shouldShowOnTextMessage () {
|
|
|
|
return !u.isVisible(this.el);
|
|
|
|
},
|
|
|
|
|
2018-04-18 10:03:21 +02:00
|
|
|
insertMessage (view) {
|
|
|
|
/* Given a view representing a message, insert it inot the
|
|
|
|
* content area of the chat box.
|
|
|
|
*
|
|
|
|
* Parameters:
|
|
|
|
* (Backbone.View) message: The message Backbone.View
|
|
|
|
*/
|
|
|
|
if (view.model.get('type') === 'error') {
|
|
|
|
const previous_msg_el = this.content.querySelector(`[data-msgid="${view.model.get('msgid')}"]`);
|
|
|
|
if (previous_msg_el) {
|
|
|
|
return previous_msg_el.insertAdjacentElement('afterend', view.el);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
const current_msg_date = moment(view.model.get('time')) || moment,
|
|
|
|
previous_msg_date = this.getLastMessageDate(current_msg_date);
|
|
|
|
|
|
|
|
if (_.isNull(previous_msg_date)) {
|
|
|
|
this.content.insertAdjacentElement('afterbegin', view.el);
|
|
|
|
} else {
|
|
|
|
const previous_msg_el = sizzle(`[data-isodate="${previous_msg_date}"]:last`, this.content).pop();
|
|
|
|
if (view.model.get('type') === 'error' &&
|
|
|
|
u.hasClass('chat-error', previous_msg_el) &&
|
|
|
|
previous_msg_el.textContent === view.model.get('message')) {
|
|
|
|
// We don't show a duplicate error message
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
previous_msg_el.insertAdjacentElement('afterend', view.el);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
showMessage (message) {
|
|
|
|
/* Inserts a chat message into the content area of the chat box.
|
|
|
|
*
|
|
|
|
* Will also insert a new day indicator if the message is on a
|
|
|
|
* different day.
|
|
|
|
*
|
|
|
|
* Parameters:
|
|
|
|
* (Backbone.Model) message: The message object
|
|
|
|
*/
|
|
|
|
const view = new _converse.MessageView({'model': message});
|
|
|
|
this.insertMessage(view);
|
|
|
|
this.insertDayIndicator(view.el);
|
|
|
|
this.clearChatStateNotification(message.get('from'));
|
|
|
|
this.setScrollPosition(view.el);
|
2018-04-16 18:08:00 +02:00
|
|
|
|
2017-12-25 12:58:30 +01:00
|
|
|
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();
|
2017-06-14 19:18:14 +02:00
|
|
|
}
|
2016-03-13 17:16:53 +01:00
|
|
|
}
|
2018-03-13 19:11:49 +01:00
|
|
|
if (this.shouldShowOnTextMessage()) {
|
|
|
|
this.show();
|
|
|
|
} else {
|
|
|
|
this.scrollDown();
|
|
|
|
}
|
2016-03-13 17:16:53 +01:00
|
|
|
},
|
|
|
|
|
2017-07-10 17:46:22 +02:00
|
|
|
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.
|
|
|
|
*/
|
2017-01-26 15:49:02 +01:00
|
|
|
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;
|
|
|
|
}
|
2016-06-22 18:47:07 +02:00
|
|
|
if (message.get('type') === 'error') {
|
2018-04-18 10:03:21 +02:00
|
|
|
this.showMessage(message);
|
2016-03-13 17:16:53 +01:00
|
|
|
} else {
|
2018-04-22 04:09:24 +02:00
|
|
|
if (message.get('chat_state') && !message.get('delayed')) {
|
2018-04-06 13:56:14 +02:00
|
|
|
this.showChatStateNotification(message);
|
2018-01-09 13:33:57 +01:00
|
|
|
}
|
2018-04-17 15:17:39 +02:00
|
|
|
if (message.get('file') || message.get('message')) {
|
2018-04-18 10:03:21 +02:00
|
|
|
this.showMessage(message);
|
2018-01-09 13:33:57 +01:00
|
|
|
}
|
2016-03-13 17:16:53 +01:00
|
|
|
}
|
2017-04-20 21:25:58 +02:00
|
|
|
_converse.emit('messageAdded', {
|
|
|
|
'message': message,
|
|
|
|
'chatbox': this.model
|
|
|
|
});
|
2016-03-13 17:16:53 +01:00
|
|
|
},
|
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2018-04-16 18:08:00 +02:00
|
|
|
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:
|
2018-02-06 17:53:56 +01:00
|
|
|
* (String) text - The chat message text.
|
|
|
|
* (String) spoiler_hint - A hint in case the message
|
2018-02-07 14:35:53 +01:00
|
|
|
* text is a hidden/spoiler message. See XEP-0382
|
2016-03-19 23:16:00 +01:00
|
|
|
*/
|
2016-12-20 10:30:20 +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
|
|
|
}
|
2018-04-17 15:17:39 +02:00
|
|
|
const attrs = this.model.getOutgoingMessageAttributes(text, spoiler_hint);
|
2018-04-16 18:08:00 +02:00
|
|
|
this.model.sendMessage(attrs);
|
2016-03-13 17:16:53 +01:00
|
|
|
},
|
2018-02-02 22:37:41 +01:00
|
|
|
|
2017-07-10 17:46:22 +02: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.
|
|
|
|
*/
|
2017-01-26 15:49:02 +01:00
|
|
|
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;
|
|
|
|
}
|
2016-12-20 10:30:20 +01:00
|
|
|
if (state === _converse.COMPOSING) {
|
2016-03-13 17:16:53 +01:00
|
|
|
this.chat_state_timeout = window.setTimeout(
|
2017-12-19 20:17:38 +01:00
|
|
|
this.setChatState.bind(this),
|
|
|
|
_converse.TIMEOUTS.PAUSED,
|
|
|
|
_converse.PAUSED
|
|
|
|
);
|
2016-12-20 10:30:20 +01:00
|
|
|
} else if (state === _converse.PAUSED) {
|
2016-03-13 17:16:53 +01:00
|
|
|
this.chat_state_timeout = window.setTimeout(
|
2017-12-19 20:17:38 +01:00
|
|
|
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) {
|
2017-03-30 12:40:17 +02:00
|
|
|
ev.preventDefault();
|
2017-06-12 20:30:58 +02:00
|
|
|
const textarea = this.el.querySelector('.chat-textarea'),
|
2017-07-15 15:58:11 +02:00
|
|
|
message = textarea.value;
|
2018-02-06 17:53:56 +01:00
|
|
|
|
|
|
|
let spoiler_hint;
|
2018-02-07 11:00:35 +01:00
|
|
|
if (this.model.get('composing_spoiler')) {
|
2018-02-06 17:53:56 +01:00
|
|
|
const hint_el = this.el.querySelector('form.sendXMPPMessage input.spoiler-hint');
|
|
|
|
spoiler_hint = hint_el.value;
|
|
|
|
hint_el.value = '';
|
|
|
|
}
|
2017-03-30 12:40:17 +02:00
|
|
|
textarea.value = '';
|
|
|
|
textarea.focus();
|
|
|
|
if (message !== '') {
|
2018-02-06 17:53:56 +01:00
|
|
|
this.onMessageSubmitted(message, spoiler_hint);
|
2017-03-30 12:40:17 +02:00
|
|
|
_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);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2017-07-10 17:46:22 +02:00
|
|
|
clearMessages (ev) {
|
2016-03-13 17:16:53 +01:00
|
|
|
if (ev && ev.preventDefault) { ev.preventDefault(); }
|
2017-06-12 20:30:58 +02:00
|
|
|
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) {
|
2017-12-24 16:46:49 +01:00
|
|
|
this.content.innerHTML = '';
|
2016-03-13 17:16:53 +01:00
|
|
|
this.model.messages.reset();
|
|
|
|
this.model.messages.browserStorage._clear();
|
|
|
|
}
|
|
|
|
return this;
|
|
|
|
},
|
|
|
|
|
2017-07-10 17:46:22 +02:00
|
|
|
insertIntoTextArea (value) {
|
2018-01-02 21:44:46 +01:00
|
|
|
const textbox_el = this.el.querySelector('.chat-textarea');
|
|
|
|
let existing = textbox_el.value;
|
2016-07-26 11:04:05 +02:00
|
|
|
if (existing && (existing[existing.length-1] !== ' ')) {
|
|
|
|
existing = existing + ' ';
|
|
|
|
}
|
2018-01-02 21:44:46 +01:00
|
|
|
textbox_el.value = existing+value+' ';
|
|
|
|
textbox_el.focus()
|
2016-07-26 11:04:05 +02:00
|
|
|
},
|
|
|
|
|
2018-02-07 11:00:35 +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
|
|
|
|
});
|
|
|
|
},
|
|
|
|
|
2017-07-15 11:55:07 +02:00
|
|
|
insertEmoji (ev) {
|
2016-03-13 17:16:53 +01:00
|
|
|
ev.stopPropagation();
|
2017-08-09 12:33:00 +02:00
|
|
|
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) {
|
2018-03-09 17:30:42 +01:00
|
|
|
if (_.isUndefined(this.emoji_dropdown)) {
|
|
|
|
ev.stopPropagation();
|
2018-03-09 14:49:56 +01:00
|
|
|
const dropdown_el = this.el.querySelector('.toggle-smiley.dropup');
|
2018-03-09 17:30:42 +01:00
|
|
|
this.emoji_dropdown = new bootstrap.Dropdown(dropdown_el, true);
|
|
|
|
this.emoji_dropdown.toggle();
|
2018-01-03 20:02:05 +01:00
|
|
|
}
|
2016-03-13 17:16:53 +01:00
|
|
|
},
|
|
|
|
|
2017-07-10 17:46:22 +02:00
|
|
|
toggleCall (ev) {
|
2016-03-13 17:16:53 +01:00
|
|
|
ev.stopPropagation();
|
2016-12-20 10:30:20 +01:00
|
|
|
_converse.emit('callButtonClicked', {
|
|
|
|
connection: _converse.connection,
|
2016-03-13 17:16:53 +01:00
|
|
|
model: this.model
|
|
|
|
});
|
|
|
|
},
|
|
|
|
|
2018-02-07 11:00:35 +01:00
|
|
|
toggleComposeSpoilerMessage () {
|
|
|
|
this.model.set('composing_spoiler', !this.model.get('composing_spoiler'));
|
|
|
|
this.renderMessageForm();
|
|
|
|
this.focus();
|
|
|
|
},
|
|
|
|
|
2018-02-06 17:53:56 +01:00
|
|
|
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") {
|
2018-02-07 14:35:53 +01:00
|
|
|
toggle_el.textContent = __('Hide hidden message');
|
2018-02-06 17:53:56 +01:00
|
|
|
toggle_el.classList.remove("icon-eye");
|
|
|
|
toggle_el.classList.add("icon-eye-blocked");
|
|
|
|
toggle_el.setAttribute("data-toggle-state", "open");
|
|
|
|
} else {
|
2018-02-07 14:35:53 +01:00
|
|
|
toggle_el.textContent = __('Show hidden message');
|
2018-02-06 17:53:56 +01:00
|
|
|
toggle_el.classList.remove("icon-eye-blocked");
|
|
|
|
toggle_el.classList.add("icon-eye");
|
|
|
|
toggle_el.setAttribute("data-toggle-state", "closed");
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2017-07-10 17:46:22 +02:00
|
|
|
onChatStatusChanged (item) {
|
2017-06-12 20:30:58 +02:00
|
|
|
const chat_status = item.get('chat_status');
|
|
|
|
let fullname = item.get('fullname');
|
2018-04-06 13:56:14 +02:00
|
|
|
let text;
|
|
|
|
|
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') {
|
2018-04-06 13:56:14 +02:00
|
|
|
text = fullname+' '+__('has gone offline');
|
2016-03-13 17:16:53 +01:00
|
|
|
} else if (chat_status === 'away') {
|
2018-04-06 13:56:14 +02:00
|
|
|
text = fullname+' '+__('has gone away');
|
2016-03-13 17:16:53 +01:00
|
|
|
} else if ((chat_status === 'dnd')) {
|
2018-04-06 13:56:14 +02:00
|
|
|
text = fullname+' '+__('is busy');
|
2016-03-13 17:16:53 +01:00
|
|
|
} else if (chat_status === 'online') {
|
2018-04-06 13:56:14 +02:00
|
|
|
text = fullname+' '+__('is online');
|
|
|
|
}
|
|
|
|
if (text) {
|
|
|
|
this.content.insertAdjacentHTML(
|
|
|
|
'beforeend',
|
|
|
|
tpl_status_message({
|
|
|
|
'message': text,
|
|
|
|
'isodate': moment().format(),
|
|
|
|
}));
|
|
|
|
this.scrollDown();
|
2016-03-13 17:16:53 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2017-07-10 17:46:22 +02:00
|
|
|
close (ev) {
|
2016-03-13 17:16:53 +01:00
|
|
|
if (ev && ev.preventDefault) { ev.preventDefault(); }
|
2017-10-31 23:04:46 +01:00
|
|
|
if (Backbone.history.getFragment() === "converse/chat?jid="+this.model.get('jid')) {
|
|
|
|
_converse.router.navigate('');
|
|
|
|
}
|
2016-12-20 10:30:20 +01:00
|
|
|
if (_converse.connection.connected) {
|
2016-03-28 13:42:33 +02:00
|
|
|
// Immediately sending the chat state, because the
|
|
|
|
// model is going to be destroyed afterwards.
|
2017-12-19 20:17:38 +01:00
|
|
|
this.setChatState(_converse.INACTIVE);
|
2018-04-22 04:20:14 +02:00
|
|
|
this.model.sendChatState();
|
2016-03-13 17:16:53 +01:00
|
|
|
}
|
2017-04-19 15:40:27 +02:00
|
|
|
try {
|
|
|
|
this.model.destroy();
|
|
|
|
} catch (e) {
|
2017-07-05 11:33:55 +02:00
|
|
|
_converse.log(e, Strophe.LogLevel.ERROR);
|
2017-04-19 15:40:27 +02:00
|
|
|
}
|
2016-03-28 13:42:33 +02:00
|
|
|
this.remove();
|
2016-12-20 10:30:20 +01:00
|
|
|
_converse.emit('chatBoxClosed', this);
|
2016-03-13 17:16:53 +01:00
|
|
|
return this;
|
|
|
|
},
|
|
|
|
|
2018-02-07 13:29:54 +01:00
|
|
|
renderEmojiPicker () {
|
|
|
|
this.emoji_picker_view.render();
|
|
|
|
},
|
|
|
|
|
2018-02-07 11:00:35 +01:00
|
|
|
insertEmojiPicker () {
|
2018-02-07 13:29:54 +01:00
|
|
|
var picker_el = this.el.querySelector('.emoji-picker');
|
|
|
|
if (!_.isNull(picker_el)) {
|
|
|
|
picker_el.innerHTML = '';
|
|
|
|
picker_el.appendChild(this.emoji_picker_view.el);
|
2018-01-29 15:16:44 +01:00
|
|
|
}
|
2016-03-13 17:16:53 +01:00
|
|
|
},
|
|
|
|
|
2017-07-10 17:46:22 +02:00
|
|
|
focus () {
|
2018-02-07 11:00:35 +01:00
|
|
|
const textarea_el = this.el.querySelector('.chat-textarea');
|
|
|
|
if (!_.isNull(textarea_el)) {
|
|
|
|
textarea_el.focus();
|
|
|
|
_converse.emit('chatBoxFocused', this);
|
|
|
|
}
|
|
|
|
return this;
|
2016-03-13 17:16:53 +01:00
|
|
|
},
|
|
|
|
|
2017-07-10 17:46:22 +02:00
|
|
|
hide () {
|
2016-11-22 17:42:58 +01:00
|
|
|
this.el.classList.add('hidden');
|
2016-03-13 17:16:53 +01:00
|
|
|
return this;
|
|
|
|
},
|
|
|
|
|
2018-03-14 14:03:13 +01:00
|
|
|
afterShown () {
|
2017-12-19 21:26:09 +01:00
|
|
|
if (u.isPersistableModel(this.model)) {
|
2018-02-14 13:44:17 +01:00
|
|
|
this.model.clearUnreadMsgCounter();
|
2016-03-28 16:52:00 +02:00
|
|
|
this.model.save();
|
|
|
|
}
|
2016-12-20 10:30:20 +01:00
|
|
|
this.setChatState(_converse.ACTIVE);
|
2018-03-09 23:16:53 +01:00
|
|
|
this.renderEmojiPicker();
|
2016-03-28 16:52:00 +02:00
|
|
|
this.scrollDown();
|
2018-03-14 20:01:33 +01:00
|
|
|
this.focus();
|
2016-03-28 16:52:00 +02:00
|
|
|
},
|
|
|
|
|
2018-03-14 14:03:13 +01:00
|
|
|
_show (f) {
|
2017-12-20 11:02:01 +01:00
|
|
|
/* Inner show method that gets debounced */
|
|
|
|
if (u.isVisible(this.el)) {
|
2018-03-14 14:03:13 +01:00
|
|
|
this.focus();
|
2016-04-01 14:46:19 +02:00
|
|
|
return;
|
|
|
|
}
|
2018-03-14 14:03:13 +01:00
|
|
|
u.fadeIn(this.el, _.bind(this.afterShown, this));
|
2016-04-01 14:46:19 +02:00
|
|
|
},
|
|
|
|
|
2018-03-14 14:03:13 +01:00
|
|
|
show () {
|
2017-12-20 11:02:01 +01:00
|
|
|
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;
|
|
|
|
},
|
|
|
|
|
2017-12-25 12:58:30 +01:00
|
|
|
showNewMessagesIndicator () {
|
|
|
|
u.showElement(this.el.querySelector('.new-msgs-indicator'));
|
|
|
|
},
|
|
|
|
|
2017-07-10 17:46:22 +02:00
|
|
|
hideNewMessagesIndicator () {
|
2017-06-12 20:30:58 +02:00
|
|
|
const new_msgs_indicator = this.el.querySelector('.new-msgs-indicator');
|
2017-03-03 15:11:25 +01:00
|
|
|
if (!_.isNull(new_msgs_indicator)) {
|
|
|
|
new_msgs_indicator.classList.add('hidden');
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2017-12-25 12:58:30 +01:00
|
|
|
_markScrolled: function (ev) {
|
2016-05-25 15:14:12 +02:00
|
|
|
/* 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(); }
|
2017-06-12 20:30:58 +02:00
|
|
|
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...
|
2017-08-09 14:07:46 +02:00
|
|
|
|
2016-05-25 15:14:12 +02:00
|
|
|
if (is_at_bottom) {
|
2017-06-19 11:08:57 +02:00
|
|
|
scrolled = false;
|
2017-05-03 09:06:28 +02:00
|
|
|
this.onScrolledDown();
|
2016-05-25 15:14:12 +02:00
|
|
|
}
|
2018-01-16 15:38:08 +01:00
|
|
|
u.safeSave(this.model, {
|
|
|
|
'scrolled': scrolled,
|
|
|
|
'top_visible_message': null
|
|
|
|
});
|
2017-08-09 14:07:46 +02:00
|
|
|
},
|
2016-05-25 15:14:12 +02:00
|
|
|
|
2017-07-10 17:46:22 +02:00
|
|
|
viewUnreadMessages () {
|
2018-01-16 15:38:08 +01:00
|
|
|
this.model.save({
|
|
|
|
'scrolled': false,
|
|
|
|
'top_visible_message': null
|
|
|
|
});
|
2016-05-28 14:25:44 +02:00
|
|
|
this.scrollDown();
|
|
|
|
},
|
|
|
|
|
2017-07-10 17:46:22 +02:00
|
|
|
_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
|
|
|
},
|
|
|
|
|
2018-02-14 13:44:17 +01:00
|
|
|
onScrolledDown () {
|
2017-12-25 12:58:30 +01:00
|
|
|
this.hideNewMessagesIndicator();
|
|
|
|
if (_converse.windowState !== 'hidden') {
|
|
|
|
this.model.clearUnreadMsgCounter();
|
|
|
|
}
|
|
|
|
_converse.emit('chatBoxScrolledDown', {'chatbox': this.model});
|
|
|
|
},
|
|
|
|
|
2017-07-10 17:46:22 +02:00
|
|
|
onWindowStateChanged (state) {
|
2017-05-16 09:21:51 +02:00
|
|
|
if (this.model.get('num_unread', 0) && !this.model.newMessageWillBeHidden()) {
|
2017-05-03 09:06:28 +02:00
|
|
|
this.model.clearUnreadMsgCounter();
|
|
|
|
}
|
2016-03-13 17:16:53 +01:00
|
|
|
}
|
|
|
|
});
|
2018-02-07 17:25:16 +01:00
|
|
|
|
|
|
|
_converse.on('connected', () => {
|
|
|
|
// Advertise that we support XEP-0382 Message Spoilers
|
|
|
|
_converse.connection.disco.addFeature(Strophe.NS.SPOILER);
|
|
|
|
});
|
2018-03-14 14:03:26 +01:00
|
|
|
|
|
|
|
/************************ BEGIN API ************************/
|
|
|
|
_.extend(_converse.api, {
|
|
|
|
'chatviews': {
|
|
|
|
'get' (jids) {
|
|
|
|
if (_.isUndefined(jids)) {
|
|
|
|
_converse.log(
|
|
|
|
"chats.create: You need to provide at least one JID",
|
|
|
|
Strophe.LogLevel.ERROR
|
|
|
|
);
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
if (_.isString(jids)) {
|
|
|
|
return _converse.chatboxviews.get(jids);
|
|
|
|
}
|
|
|
|
return _.map(jids, (jid) => _converse.chatboxviews.get(jids));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
/************************ END API ************************/
|
2016-03-13 17:16:53 +01:00
|
|
|
}
|
|
|
|
});
|
2017-02-14 16:34:32 +01:00
|
|
|
|
|
|
|
return converse;
|
2016-03-13 17:16:53 +01:00
|
|
|
}));
|