From fe47773c7f784c23fe51af2034527a4295c05407 Mon Sep 17 00:00:00 2001 From: JC Brand Date: Sun, 13 Mar 2016 16:16:53 +0000 Subject: [PATCH] Move ChatView into separate plugin. --- converse.js | 4 +- src/converse-chatview.js | 934 +++++++++++++++++++++++++++++++++++++ src/converse-controlbox.js | 10 +- src/converse-core.js | 895 +---------------------------------- src/converse-headline.js | 19 +- src/converse-muc.js | 11 +- 6 files changed, 973 insertions(+), 900 deletions(-) create mode 100644 src/converse-chatview.js diff --git a/converse.js b/converse.js index 13deaf649..c32c2e0fa 100644 --- a/converse.js +++ b/converse.js @@ -45,9 +45,10 @@ require.config({ // Converse "converse-api": "src/converse-api", + "converse-chatview": "src/converse-chatview", "converse-controlbox": "src/converse-controlbox", "converse-core": "src/converse-core", - "converse-headline": "src/converse-notification", + "converse-headline": "src/converse-headline", "converse-muc": "src/converse-muc", "converse-notification": "src/converse-notification", "converse-otr": "src/converse-otr", @@ -225,6 +226,7 @@ if (typeof define !== 'undefined') { // file src/locales.js to include only those // translations that you care about. + "converse-chatview", // Renders standalone chat boxes for single user chat "converse-muc", // XEP-0045 Multi-user chat "converse-otr", // Off-the-record encryption for one-on-one messages "converse-controlbox", // The control box diff --git a/src/converse-chatview.js b/src/converse-chatview.js new file mode 100644 index 000000000..3b227478d --- /dev/null +++ b/src/converse-chatview.js @@ -0,0 +1,934 @@ +// Converse.js (A browser based XMPP chat client) +// http://conversejs.org +// +// Copyright (c) 2012-2016, Jan-Carel Brand +// Licensed under the Mozilla Public License (MPLv2) +// +/*global Backbone, define */ + +(function (root, factory) { + define("converse-chatview", ["converse-core", "converse-api"], factory); +}(this, function (converse, converse_api) { + "use strict"; + var $ = converse_api.env.jQuery, + utils = converse_api.env.utils, + Strophe = converse_api.env.Strophe, + $msg = converse_api.env.$msg, + _ = converse_api.env._, + __ = utils.__.bind(converse), + moment = converse_api.env.moment; + + var KEY = { + ENTER: 13, + FORWARD_SLASH: 47 + }; + + + converse_api.plugins.add('chatview', { + + 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. + + ChatBoxViews: { + onChatBoxAdded: function (item) { + var view = this.get(item.get('id')); + // FIXME: leaky abstraction from chatroom here, need to + // come up with a nicer solution for this. + // Perhaps change 'chatroom' to more generic non-boolean + if (!view && !item.get('chatroom')) { + view = new converse.ChatBoxView({model: item}); + this.add(item.get('id'), view); + this.trimChats(view); + } else { + this._super.onChatBoxAdded.apply(this, arguments); + } + } + } + }, + + + initialize: function () { + /* The initialize function gets called as soon as the plugin is + * loaded by converse.js's plugin machinery. + */ + this.updateSettings({ + show_toolbar: true, + }); + + converse.ChatBoxView = Backbone.View.extend({ + length: 200, + tagName: 'div', + className: 'chatbox', + is_chatroom: false, // This is not a multi-user chatroom + + events: { + 'click .close-chatbox-button': 'close', + 'click .toggle-chatbox-button': 'minimize', + 'keypress textarea.chat-textarea': 'keyPressed', + 'click .toggle-smiley': 'toggleEmoticonMenu', + 'click .toggle-smiley ul li': 'insertEmoticon', + 'click .toggle-clear': 'clearMessages', + 'click .toggle-call': 'toggleCall', + 'mousedown .dragresize-top': 'onStartVerticalResize', + 'mousedown .dragresize-left': 'onStartHorizontalResize', + 'mousedown .dragresize-topleft': 'onStartDiagonalResize' + }, + + initialize: function () { + $(window).on('resize', _.debounce(this.setDimensions.bind(this), 100)); + this.model.messages.on('add', this.onMessageAdded, this); + this.model.on('show', this.show, this); + this.model.on('destroy', this.hide, this); + // 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('change:image', this.renderAvatar, this); + this.model.on('change:minimized', this.onMinimizedChanged, this); + this.model.on('change:status', this.onStatusChanged, this); + this.model.on('showHelpMessages', this.showHelpMessages, this); + this.model.on('sendMessage', this.sendMessage, this); + this.updateVCard().render().fetchMessages().insertIntoPage().hide(); + }, + + render: function () { + this.$el.attr('id', this.model.get('box_id')) + .html(converse.templates.chatbox( + _.extend(this.model.toJSON(), { + show_toolbar: converse.show_toolbar, + show_textarea: true, + title: this.model.get('fullname'), + info_close: __('Close this chat box'), + info_minimize: __('Minimize this chat box'), + label_personal_message: __('Personal message') + } + ) + ) + ); + this.setWidth(); + this.$content = this.$el.find('.chat-content'); + this.renderToolbar().renderAvatar(); + this.$content.on('scroll', _.debounce(this.onScroll.bind(this), 100)); + converse.emit('chatBoxOpened', this); + window.setTimeout(utils.refreshWebkit, 50); + return this.showStatusMessage(); + }, + + setWidth: function () { + // If a custom width is applied (due to drag-resizing), + // then we need to set the width of the .chatbox element as well. + if (this.model.get('width')) { + this.$el.css('width', this.model.get('width')); + } + }, + + onScroll: function (ev) { + if ($(ev.target).scrollTop() === 0 && this.model.messages.length) { + this.fetchArchivedMessages({ + 'before': this.model.messages.at(0).get('archive_id'), + 'with': this.model.get('jid'), + 'max': converse.archived_messages_page_size + }); + } + }, + + fetchMessages: function () { + /* Responsible for fetching previously sent messages, first + * from session storage, and then once that's done by calling + * fetchArchivedMessages, which fetches from the XMPP server if + * applicable. + */ + this.model.messages.fetch({ + 'add': true, + 'success': function () { + if (!converse.features.findWhere({'var': Strophe.NS.MAM})) { + return; + } + if (this.model.messages.length < converse.archived_messages_page_size) { + this.fetchArchivedMessages({ + 'before': '', // Page backwards from the most recent message + 'with': this.model.get('jid'), + 'max': converse.archived_messages_page_size + }); + } + }.bind(this) + }); + return this; + }, + + fetchArchivedMessages: function (options) { + /* Fetch archived chat messages from the XMPP server. + * + * Then, upon receiving them, call onMessage on the chat box, + * so that they are displayed inside it. + */ + if (!converse.features.findWhere({'var': Strophe.NS.MAM})) { + converse.log("Attempted to fetch archived messages but this user's server doesn't support XEP-0313"); + return; + } + this.addSpinner(); + converse.queryForArchivedMessages(options, function (messages) { + this.clearSpinner(); + if (messages.length) { + _.map(messages, converse.chatboxes.onMessage.bind(converse.chatboxes)); + } + }.bind(this), + function () { + this.clearSpinner(); + converse.log("Error or timeout while trying to fetch archived messages", "error"); + }.bind(this) + ); + }, + + insertIntoPage: function () { + /* This method gets overridden in src/converse-controlbox.js if + * the controlbox plugin is active. + */ + $('#conversejs').prepend(this.$el); + return this; + }, + + adjustToViewport: function () { + /* Event handler called when viewport gets resized. We remove + * custom width/height from chat boxes. + */ + var viewport_width = Math.max(document.documentElement.clientWidth, window.innerWidth || 0); + var viewport_height = Math.max(document.documentElement.clientHeight, window.innerHeight || 0); + if (viewport_width <= 480) { + this.model.set('height', undefined); + this.model.set('width', undefined); + } else if (viewport_width <= this.model.get('width')) { + this.model.set('width', undefined); + } else if (viewport_height <= this.model.get('height')) { + this.model.set('height', undefined); + } + }, + + initDragResize: function () { + /* Determine and store the default box size. + * We need this information for the drag-resizing feature. + */ + var $flyout = this.$el.find('.box-flyout'); + if (typeof this.model.get('height') === 'undefined') { + var height = $flyout.height(); + var width = $flyout.width(); + this.model.set('height', height); + this.model.set('default_height', height); + this.model.set('width', width); + this.model.set('default_width', width); + } + var min_width = $flyout.css('min-width'); + var min_height = $flyout.css('min-height'); + this.model.set('min_width', min_width.endsWith('px') ? Number(min_width.replace(/px$/, '')) :0); + this.model.set('min_height', min_height.endsWith('px') ? Number(min_height.replace(/px$/, '')) :0); + // Initialize last known mouse position + this.prev_pageY = 0; + this.prev_pageX = 0; + if (converse.connection.connected) { + this.height = this.model.get('height'); + this.width = this.model.get('width'); + } + return this; + }, + + setDimensions: function () { + // Make sure the chat box has the right height and width. + this.adjustToViewport(); + this.setChatBoxHeight(this.model.get('height')); + this.setChatBoxWidth(this.model.get('width')); + }, + + clearStatusNotification: function () { + this.$content.find('div.chat-event').remove(); + }, + + showStatusNotification: function (message, keep_old) { + if (!keep_old) { + this.clearStatusNotification(); + } + var was_at_bottom = this.$content.scrollTop() + this.$content.innerHeight() >= this.$content[0].scrollHeight; + this.$content.append($('
').text(message)); + if (was_at_bottom) { + this.scrollDown(); + } + }, + + addSpinner: function () { + if (!this.$content.first().hasClass('spinner')) { + this.$content.prepend(''); + } + }, + + clearSpinner: function () { + if (this.$content.children(':first').is('span.spinner')) { + this.$content.children(':first').remove(); + } + }, + + prependDayIndicator: function (date) { + /* Prepends an indicator into the chat area, showing the day as + * given by the passed in date. + * + * Parameters: + * (String) date - An ISO8601 date string. + */ + var day_date = moment(date).startOf('day'); + this.$content.prepend(converse.templates.new_day({ + isodate: day_date.format(), + datestring: day_date.format("dddd MMM Do YYYY") + })); + }, + + appendMessage: function (attrs) { + /* Helper method which appends a message to the end of the chat + * box's content area. + * + * Parameters: + * (Object) attrs: An object containing the message attributes. + */ + _.compose( + _.debounce(this.scrollDown.bind(this), 50), + this.$content.append.bind(this.$content) + )(this.renderMessage(attrs)); + }, + + showMessage: function (attrs) { + /* 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. + * + * 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. + */ + var $first_msg = this.$content.children('.chat-message:first'), + first_msg_date = $first_msg.data('isodate'), + last_msg_date, current_msg_date, day_date, $msgs, msg_dates, idx; + if (!first_msg_date) { + this.appendMessage(attrs); + return; + } + current_msg_date = moment(attrs.time) || moment; + last_msg_date = this.$content.children('.chat-message:last').data('isodate'); + + if (typeof last_msg_date !== "undefined" && (current_msg_date.isAfter(last_msg_date) || current_msg_date.isSame(last_msg_date))) { + // The new message is after the last message + if (current_msg_date.isAfter(last_msg_date, 'day')) { + // Append a new day indicator + day_date = moment(current_msg_date).startOf('day'); + this.$content.append(converse.templates.new_day({ + isodate: current_msg_date.format(), + datestring: current_msg_date.format("dddd MMM Do YYYY") + })); + } + this.appendMessage(attrs); + return; + } + + if (typeof first_msg_date !== "undefined" && + (current_msg_date.isBefore(first_msg_date) || + (current_msg_date.isSame(first_msg_date) && !current_msg_date.isSame(last_msg_date)))) { + // The new message is before the first message + + if ($first_msg.prev().length === 0) { + // There's no day indicator before the first message, so we prepend one. + this.prependDayIndicator(first_msg_date); + } + if (current_msg_date.isBefore(first_msg_date, 'day')) { + _.compose( + this.scrollDownMessageHeight.bind(this), + function ($el) { + this.$content.prepend($el); + return $el; + }.bind(this) + )(this.renderMessage(attrs)); + // This message is on a different day, so we add a day indicator. + this.prependDayIndicator(current_msg_date); + } else { + // The message is before the first, but on the same day. + // We need to prepend the message immediately before the + // first message (so that it'll still be after the day indicator). + _.compose( + this.scrollDownMessageHeight.bind(this), + function ($el) { + $el.insertBefore($first_msg); + return $el; + } + )(this.renderMessage(attrs)); + } + } else { + // We need to find the correct place to position the message + current_msg_date = current_msg_date.format(); + $msgs = this.$content.children('.chat-message'); + msg_dates = _.map($msgs, function (el) { + return $(el).data('isodate'); + }); + msg_dates.push(current_msg_date); + msg_dates.sort(); + idx = msg_dates.indexOf(current_msg_date)-1; + _.compose( + this.scrollDownMessageHeight.bind(this), + function ($el) { + $el.insertAfter(this.$content.find('.chat-message[data-isodate="'+msg_dates[idx]+'"]')); + return $el; + }.bind(this) + )(this.renderMessage(attrs)); + } + }, + + renderMessage: function (attrs) { + /* Renders a chat message based on the passed in attributes. + * + * Parameters: + * (Object) attrs: An object containing the message attributes. + * + * Returns: + * The DOM element representing the message. + */ + var msg_time = moment(attrs.time) || moment, + text = attrs.message, + match = text.match(/^\/(.*?)(?: (.*))?$/), + fullname = this.model.get('fullname') || attrs.fullname, + extra_classes = attrs.delayed && 'delayed' || '', + template, username; + + if ((match) && (match[1] === 'me')) { + text = text.replace(/^\/me/, ''); + template = converse.templates.action; + username = fullname; + } else { + template = converse.templates.message; + username = attrs.sender === 'me' && __('me') || fullname; + } + this.$content.find('div.chat-event').remove(); + + // FIXME: leaky abstraction from MUC + if (this.is_chatroom && attrs.sender === 'them' && (new RegExp("\\b"+this.model.get('nick')+"\\b")).test(text)) { + // Add special class to mark groupchat messages in which we + // are mentioned. + extra_classes += ' mentioned'; + } + return $(template({ + msgid: attrs.msgid, + 'sender': attrs.sender, + 'time': msg_time.format('hh:mm'), + 'isodate': msg_time.format(), + 'username': username, + 'message': '', + 'extra_classes': extra_classes + })).children('.chat-msg-content').first().text(text) + .addHyperlinks() + .addEmoticons(converse.visible_toolbar_buttons.emoticons).parent(); + }, + + showHelpMessages: function (msgs, type, spinner) { + var i, msgs_length = msgs.length; + for (i=0; i'+msgs[i]+'')); + } + if (spinner === true) { + this.$content.append(''); + } else if (spinner === false) { + this.$content.find('span.spinner').remove(); + } + return this.scrollDown(); + }, + + handleChatStateMessage: function (message) { + if (message.get('chat_state') === converse.COMPOSING) { + this.showStatusNotification(message.get('fullname')+' '+__('is typing')); + this.clear_status_timeout = window.setTimeout(this.clearStatusNotification.bind(this), 10000); + } else if (message.get('chat_state') === converse.PAUSED) { + this.showStatusNotification(message.get('fullname')+' '+__('has stopped typing')); + } else if (_.contains([converse.INACTIVE, converse.ACTIVE], message.get('chat_state'))) { + this.$content.find('div.chat-event').remove(); + } else if (message.get('chat_state') === converse.GONE) { + this.showStatusNotification(message.get('fullname')+' '+__('has gone away')); + } + }, + + handleTextMessage: function (message) { + this.showMessage(_.clone(message.attributes)); + if ((message.get('sender') !== 'me') && (converse.windowState === 'blur')) { + converse.incrementMsgCounter(); + } + if (!this.model.get('minimized') && !this.$el.is(':visible')) { + this.show(); + } + }, + + onMessageAdded: function (message) { + /* Handler that gets called when a new message object is created. + * + * Parameters: + * (Object) message - The message Backbone object that was added. + */ + if (typeof this.clear_status_timeout !== 'undefined') { + window.clearTimeout(this.clear_status_timeout); + delete this.clear_status_timeout; + } + if (!message.get('message')) { + this.handleChatStateMessage(message); + } else { + this.handleTextMessage(message); + } + }, + + createMessageStanza: function (message) { + return $msg({ + from: converse.connection.jid, + to: this.model.get('jid'), + type: 'chat', + id: message.get('msgid') + }).c('body').t(message.get('message')).up() + .c(converse.ACTIVE, {'xmlns': Strophe.NS.CHATSTATES}).up(); + }, + + sendMessage: function (message) { + /* Responsible for sending off a text message. + * + * Parameters: + * (Message) message - The chat message + */ + // TODO: We might want to send to specfic resources. + // Especially in the OTR case. + var messageStanza = this.createMessageStanza(message); + converse.connection.send(messageStanza); + if (converse.forward_messages) { + // 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:'urn:xmpp:forward:0'}) + .c('delay', {xmns:'urn:xmpp:delay',stamp:(new Date()).getTime()}).up() + .cnode(messageStanza.tree()) + ); + } + }, + + onMessageSubmitted: function (text) { + /* This method gets called once the user has typed a message + * and then pressed enter in a chat box. + * + * Parameters: + * (string) text - The chat message text. + */ + if (!converse.connection.authenticated) { + return this.showHelpMessages( + ['Sorry, the connection has been lost, '+ + 'and your message could not be sent'], + 'error' + ); + } + var match = text.replace(/^\s*/, "").match(/^\/(.*)\s*$/), msgs; + if (match) { + if (match[1] === "clear") { + return this.clearMessages(); + } + else if (match[1] === "help") { + msgs = [ + '/help:'+__('Show this menu')+'', + '/me:'+__('Write in the third person')+'', + '/clear:'+__('Remove messages')+'' + ]; + this.showHelpMessages(msgs); + return; + } + } + var fullname = converse.xmppstatus.get('fullname'); + fullname = _.isEmpty(fullname)? converse.bare_jid: fullname; + var message = this.model.messages.create({ + fullname: fullname, + sender: 'me', + time: moment().format(), + message: text + }); + this.sendMessage(message); + }, + + sendChatState: function () { + /* Sends a message with the status of the user in this chat session + * as taken from the 'chat_state' attribute of the chat box. + * See XEP-0085 Chat State Notifications. + */ + converse.connection.send( + $msg({'to':this.model.get('jid'), 'type': 'chat'}) + .c(this.model.get('chat_state'), {'xmlns': Strophe.NS.CHATSTATES}) + ); + }, + + setChatState: function (state, no_save) { + /* Mutator for setting the chat state of this chat session. + * 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 (typeof this.chat_state_timeout !== 'undefined') { + window.clearTimeout(this.chat_state_timeout); + delete this.chat_state_timeout; + } + if (state === converse.COMPOSING) { + this.chat_state_timeout = window.setTimeout( + this.setChatState.bind(this), converse.TIMEOUTS.PAUSED, converse.PAUSED); + } else if (state === converse.PAUSED) { + this.chat_state_timeout = window.setTimeout( + this.setChatState.bind(this), converse.TIMEOUTS.INACTIVE, converse.INACTIVE); + } + if (!no_save && this.model.get('chat_state') !== state) { + this.model.set('chat_state', state); + } + return this; + }, + + keyPressed: function (ev) { + /* Event handler for when a key is pressed in a chat box textarea. + */ + var $textarea = $(ev.target), message; + if (ev.keyCode === KEY.ENTER) { + ev.preventDefault(); + message = $textarea.val(); + $textarea.val('').focus(); + if (message !== '') { + if (this.model.get('chatroom')) { + this.onChatRoomMessageSubmitted(message); + } else { + this.onMessageSubmitted(message); + } + converse.emit('messageSend', message); + } + this.setChatState(converse.ACTIVE); + } else if (!this.model.get('chatroom')) { // chat state data is currently only for single user chat + // 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); + } + }, + + onStartVerticalResize: function (ev) { + if (!converse.allow_dragresize) { return true; } + // Record element attributes for mouseMove(). + this.height = this.$el.children('.box-flyout').height(); + converse.resizing = { + 'chatbox': this, + 'direction': 'top' + }; + this.prev_pageY = ev.pageY; + }, + + onStartHorizontalResize: function (ev) { + if (!converse.allow_dragresize) { return true; } + this.width = this.$el.children('.box-flyout').width(); + converse.resizing = { + 'chatbox': this, + 'direction': 'left' + }; + this.prev_pageX = ev.pageX; + }, + + onStartDiagonalResize: function (ev) { + this.onStartHorizontalResize(ev); + this.onStartVerticalResize(ev); + converse.resizing.direction = 'topleft'; + }, + + setChatBoxHeight: function (height) { + if (!this.model.get('minimized')) { + if (height) { + height = converse.applyDragResistance(height, this.model.get('default_height'))+'px'; + } else { + height = ""; + } + this.$el.children('.box-flyout')[0].style.height = height; + } + }, + + setChatBoxWidth: function (width) { + if (!this.model.get('minimized')) { + if (width) { + width = converse.applyDragResistance(width, this.model.get('default_width'))+'px'; + } else { + width = ""; + } + this.$el[0].style.width = width; + this.$el.children('.box-flyout')[0].style.width = width; + } + }, + + resizeChatBox: function (ev) { + var diff; + if (converse.resizing.direction.indexOf('top') === 0) { + diff = ev.pageY - this.prev_pageY; + if (diff) { + this.height = ((this.height-diff) > (this.model.get('min_height') || 0)) ? (this.height-diff) : this.model.get('min_height'); + this.prev_pageY = ev.pageY; + this.setChatBoxHeight(this.height); + } + } + if (converse.resizing.direction.indexOf('left') !== -1) { + diff = this.prev_pageX - ev.pageX; + if (diff) { + this.width = ((this.width+diff) > (this.model.get('min_width') || 0)) ? (this.width+diff) : this.model.get('min_width'); + this.prev_pageX = ev.pageX; + this.setChatBoxWidth(this.width); + } + } + }, + + clearMessages: function (ev) { + if (ev && ev.preventDefault) { ev.preventDefault(); } + var result = confirm(__("Are you sure you want to clear the messages from this chat box?")); + if (result === true) { + this.$content.empty(); + this.model.messages.reset(); + this.model.messages.browserStorage._clear(); + } + return this; + }, + + insertEmoticon: function (ev) { + ev.stopPropagation(); + this.$el.find('.toggle-smiley ul').slideToggle(200); + var $textbox = this.$el.find('textarea.chat-textarea'); + var value = $textbox.val(); + var $target = $(ev.target); + $target = $target.is('a') ? $target : $target.children('a'); + if (value && (value[value.length-1] !== ' ')) { + value = value + ' '; + } + $textbox.focus().val(value+$target.data('emoticon')+' '); + }, + + toggleEmoticonMenu: function (ev) { + ev.stopPropagation(); + this.$el.find('.toggle-smiley ul').slideToggle(200); + }, + + toggleCall: function (ev) { + ev.stopPropagation(); + converse.emit('callButtonClicked', { + connection: converse.connection, + model: this.model + }); + }, + + onChatStatusChanged: function (item) { + var chat_status = item.get('chat_status'), + fullname = item.get('fullname'); + fullname = _.isEmpty(fullname)? item.get('jid'): fullname; + if (this.$el.is(':visible')) { + 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') { + this.$el.find('div.chat-event').remove(); + } + } + }, + + onStatusChanged: function (item) { + this.showStatusMessage(); + converse.emit('contactStatusMessageChanged', { + 'contact': item.attributes, + 'message': item.get('status') + }); + }, + + onMinimizedChanged: function (item) { + if (item.get('minimized')) { + this.minimize(); + } else { + this.maximize(); + } + }, + + showStatusMessage: function (msg) { + msg = msg || this.model.get('status'); + if (typeof msg === "string") { + this.$el.find('p.user-custom-message').text(msg).attr('title', msg); + } + return this; + }, + + close: function (ev) { + if (ev && ev.preventDefault) { ev.preventDefault(); } + if (converse.connection.connected) { + this.model.destroy(); + this.setChatState(converse.INACTIVE); + } else { + this.hide(); + } + converse.emit('chatBoxClosed', this); + return this; + }, + + onMaximized: function () { + converse.chatboxviews.trimChats(this); + utils.refreshWebkit(); + this.$content.scrollTop(this.model.get('scroll')); + this.setChatState(converse.ACTIVE).focus(); + converse.emit('chatBoxMaximized', this); + }, + + onMinimized: function () { + utils.refreshWebkit(); + converse.emit('chatBoxMinimized', this); + }, + + maximize: function () { + // Restore a minimized chat box + $('#conversejs').prepend(this.$el); + this.$el.show('fast', this.onMaximized.bind(this)); + return this; + }, + + minimize: function (ev) { + if (ev && ev.preventDefault) { ev.preventDefault(); } + // save the scroll position to restore it on maximize + this.model.save({'scroll': this.$content.scrollTop()}); + this.setChatState(converse.INACTIVE).model.minimize(); + this.$el.hide('fast', this.onMinimized.bind(this)); + }, + + updateVCard: function () { + if (!this.use_vcards) { return this; } + var jid = this.model.get('jid'), + contact = converse.roster.get(jid); + if ((contact) && (!contact.get('vcard_updated'))) { + converse.getVCard( + jid, + function (iq, jid, fullname, image, image_type, url) { + this.model.save({ + 'fullname' : fullname || jid, + 'url': url, + 'image_type': image_type, + 'image': image + }); + }.bind(this), + function () { + converse.log("ChatBoxView.initialize: An error occured while fetching vcard"); + } + ); + } + return this; + }, + + renderToolbar: function (options) { + if (!converse.show_toolbar) { + return; + } + options = _.extend(options || {}, { + label_clear: __('Clear all messages'), + label_hide_occupants: __('Hide the list of occupants'), + 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, + show_emoticons: converse.visible_toolbar_buttons.emoticons, + // FIXME Leaky abstraction MUC + show_occupants_toggle: this.is_chatroom && converse.visible_toolbar_buttons.toggle_occupants + }); + this.$el.find('.chat-toolbar').html(converse.templates.toolbar(_.extend(this.model.toJSON(), options || {}))); + return this; + }, + + renderAvatar: function () { + if (!this.model.get('image')) { + return; + } + var img_src = 'data:'+this.model.get('image_type')+';base64,'+this.model.get('image'), + canvas = $('').get(0); + + if (!(canvas.getContext && canvas.getContext('2d'))) { + return this; + } + var ctx = canvas.getContext('2d'); + var img = new Image(); // Create new Image object + img.onload = function () { + var ratio = img.width/img.height; + if (ratio < 1) { + ctx.drawImage(img, 0,0, 32, 32*(1/ratio)); + } else { + ctx.drawImage(img, 0,0, 32, 32*ratio); + } + + }; + img.src = img_src; + this.$el.find('.chat-title').before(canvas); + return this; + }, + + focus: function () { + this.$el.find('.chat-textarea').focus(); + converse.emit('chatBoxFocused', this); + return this; + }, + + hide: function () { + if (this.$el.is(':visible') && this.$el.css('opacity') === "1") { + this.$el.hide(); + utils.refreshWebkit(); + } + return this; + }, + + show: function (focus) { + if (typeof this.debouncedShow === 'undefined') { + /* 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(function (focus) { + if (this.$el.is(':visible') && this.$el.css('opacity') === "1") { + if (focus) { this.focus(); } + return; + } + this.initDragResize().setDimensions(); + this.$el.fadeIn(function () { + if (converse.connection.connected) { + // Without a connection, we haven't yet initialized + // localstorage + this.model.save(); + } + converse.chatboxviews.trimChats(this); + this.setChatState(converse.ACTIVE); + this.scrollDown(); + if (focus) { + this.focus(); + } + }.bind(this)); + }, 250, true); + } + this.debouncedShow.apply(this, arguments); + return this; + }, + + scrollDownMessageHeight: function ($message) { + if (this.$content.is(':visible')) { + this.$content.scrollTop(this.$content.scrollTop() + $message[0].scrollHeight); + } + return this; + }, + + scrollDown: function () { + if (this.$content.is(':visible')) { + this.$content.scrollTop(this.$content[0].scrollHeight); + } + return this; + } + }); + } + }); +})); diff --git a/src/converse-controlbox.js b/src/converse-controlbox.js index 20077d538..66abbfa55 100644 --- a/src/converse-controlbox.js +++ b/src/converse-controlbox.js @@ -7,7 +7,12 @@ /*global define, Backbone */ (function (root, factory) { - define("converse-controlbox", ["converse-core", "converse-api"], factory); + define("converse-controlbox", [ + "converse-core", + "converse-api", + // TODO: remove this dependency + "converse-chatview" + ], factory); }(this, function (converse, converse_api) { "use strict"; // Strophe methods for building stanzas @@ -18,10 +23,9 @@ // Other necessary globals var $ = converse_api.env.jQuery, _ = converse_api.env._, + __ = utils.__.bind(converse), moment = converse_api.env.moment; - // For translations - var __ = utils.__.bind(converse); converse_api.plugins.add('controlbox', { diff --git a/src/converse-core.js b/src/converse-core.js index 206f2f3f7..eeb5dc42a 100755 --- a/src/converse-core.js +++ b/src/converse-core.js @@ -40,7 +40,6 @@ // Strophe globals var $build = Strophe.$build; var $iq = Strophe.$iq; - var $msg = Strophe.$msg; var $pres = Strophe.$pres; var b64_sha1 = Strophe.SHA1.b64_sha1; Strophe = Strophe.Strophe; @@ -95,11 +94,6 @@ converse.OPENED = 'opened'; converse.CLOSED = 'closed'; - var KEY = { - ENTER: 13, - FORWARD_SLASH: 47 - }; - var PRETTY_CONNECTION_STATUS = { 0: 'ERROR', 1: 'CONNECTING', @@ -381,7 +375,6 @@ rid: undefined, roster_groups: false, show_only_online_users: false, - show_toolbar: true, sid: undefined, storage: 'session', synchronize_availability: true, // Set to false to not sync with other clients or with resource name of the particular client that it should synchronize with @@ -1428,876 +1421,6 @@ } }); - this.ChatBoxView = Backbone.View.extend({ - length: 200, - tagName: 'div', - className: 'chatbox', - is_chatroom: false, // This is not a multi-user chatroom - - events: { - 'click .close-chatbox-button': 'close', - 'click .toggle-chatbox-button': 'minimize', - 'keypress textarea.chat-textarea': 'keyPressed', - 'click .toggle-smiley': 'toggleEmoticonMenu', - 'click .toggle-smiley ul li': 'insertEmoticon', - 'click .toggle-clear': 'clearMessages', - 'click .toggle-call': 'toggleCall', - 'mousedown .dragresize-top': 'onStartVerticalResize', - 'mousedown .dragresize-left': 'onStartHorizontalResize', - 'mousedown .dragresize-topleft': 'onStartDiagonalResize' - }, - - initialize: function () { - $(window).on('resize', _.debounce(this.setDimensions.bind(this), 100)); - this.model.messages.on('add', this.onMessageAdded, this); - this.model.on('show', this.show, this); - this.model.on('destroy', this.hide, this); - // 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('change:image', this.renderAvatar, this); - this.model.on('change:minimized', this.onMinimizedChanged, this); - this.model.on('change:status', this.onStatusChanged, this); - this.model.on('showHelpMessages', this.showHelpMessages, this); - this.model.on('sendMessage', this.sendMessage, this); - this.updateVCard().render().fetchMessages().insertIntoPage().hide(); - }, - - render: function () { - this.$el.attr('id', this.model.get('box_id')) - .html(converse.templates.chatbox( - _.extend(this.model.toJSON(), { - show_toolbar: converse.show_toolbar, - show_textarea: true, - title: this.model.get('fullname'), - info_close: __('Close this chat box'), - info_minimize: __('Minimize this chat box'), - label_personal_message: __('Personal message') - } - ) - ) - ); - this.setWidth(); - this.$content = this.$el.find('.chat-content'); - this.renderToolbar().renderAvatar(); - this.$content.on('scroll', _.debounce(this.onScroll.bind(this), 100)); - converse.emit('chatBoxOpened', this); - window.setTimeout(utils.refreshWebkit, 50); - return this.showStatusMessage(); - }, - - setWidth: function () { - // If a custom width is applied (due to drag-resizing), - // then we need to set the width of the .chatbox element as well. - if (this.model.get('width')) { - this.$el.css('width', this.model.get('width')); - } - }, - - onScroll: function (ev) { - if ($(ev.target).scrollTop() === 0 && this.model.messages.length) { - this.fetchArchivedMessages({ - 'before': this.model.messages.at(0).get('archive_id'), - 'with': this.model.get('jid'), - 'max': converse.archived_messages_page_size - }); - } - }, - - fetchMessages: function () { - /* Responsible for fetching previously sent messages, first - * from session storage, and then once that's done by calling - * fetchArchivedMessages, which fetches from the XMPP server if - * applicable. - */ - this.model.messages.fetch({ - 'add': true, - 'success': function () { - if (!converse.features.findWhere({'var': Strophe.NS.MAM})) { - return; - } - if (this.model.messages.length < converse.archived_messages_page_size) { - this.fetchArchivedMessages({ - 'before': '', // Page backwards from the most recent message - 'with': this.model.get('jid'), - 'max': converse.archived_messages_page_size - }); - } - }.bind(this) - }); - return this; - }, - - fetchArchivedMessages: function (options) { - /* Fetch archived chat messages from the XMPP server. - * - * Then, upon receiving them, call onMessage on the chat box, - * so that they are displayed inside it. - */ - if (!converse.features.findWhere({'var': Strophe.NS.MAM})) { - converse.log("Attempted to fetch archived messages but this user's server doesn't support XEP-0313"); - return; - } - this.addSpinner(); - converse.queryForArchivedMessages(options, function (messages) { - this.clearSpinner(); - if (messages.length) { - _.map(messages, converse.chatboxes.onMessage.bind(converse.chatboxes)); - } - }.bind(this), - function () { - this.clearSpinner(); - converse.log("Error or timeout while trying to fetch archived messages", "error"); - }.bind(this) - ); - }, - - insertIntoPage: function () { - /* This method gets overridden in src/converse-controlbox.js if - * the controlbox plugin is active. - */ - $('#conversejs').prepend(this.$el); - return this; - }, - - adjustToViewport: function () { - /* Event handler called when viewport gets resized. We remove - * custom width/height from chat boxes. - */ - var viewport_width = Math.max(document.documentElement.clientWidth, window.innerWidth || 0); - var viewport_height = Math.max(document.documentElement.clientHeight, window.innerHeight || 0); - if (viewport_width <= 480) { - this.model.set('height', undefined); - this.model.set('width', undefined); - } else if (viewport_width <= this.model.get('width')) { - this.model.set('width', undefined); - } else if (viewport_height <= this.model.get('height')) { - this.model.set('height', undefined); - } - }, - - initDragResize: function () { - /* Determine and store the default box size. - * We need this information for the drag-resizing feature. - */ - var $flyout = this.$el.find('.box-flyout'); - if (typeof this.model.get('height') === 'undefined') { - var height = $flyout.height(); - var width = $flyout.width(); - this.model.set('height', height); - this.model.set('default_height', height); - this.model.set('width', width); - this.model.set('default_width', width); - } - var min_width = $flyout.css('min-width'); - var min_height = $flyout.css('min-height'); - this.model.set('min_width', min_width.endsWith('px') ? Number(min_width.replace(/px$/, '')) :0); - this.model.set('min_height', min_height.endsWith('px') ? Number(min_height.replace(/px$/, '')) :0); - // Initialize last known mouse position - this.prev_pageY = 0; - this.prev_pageX = 0; - if (converse.connection.connected) { - this.height = this.model.get('height'); - this.width = this.model.get('width'); - } - return this; - }, - - setDimensions: function () { - // Make sure the chat box has the right height and width. - this.adjustToViewport(); - this.setChatBoxHeight(this.model.get('height')); - this.setChatBoxWidth(this.model.get('width')); - }, - - clearStatusNotification: function () { - this.$content.find('div.chat-event').remove(); - }, - - showStatusNotification: function (message, keep_old) { - if (!keep_old) { - this.clearStatusNotification(); - } - var was_at_bottom = this.$content.scrollTop() + this.$content.innerHeight() >= this.$content[0].scrollHeight; - this.$content.append($('
').text(message)); - if (was_at_bottom) { - this.scrollDown(); - } - }, - - addSpinner: function () { - if (!this.$content.first().hasClass('spinner')) { - this.$content.prepend(''); - } - }, - - clearSpinner: function () { - if (this.$content.children(':first').is('span.spinner')) { - this.$content.children(':first').remove(); - } - }, - - prependDayIndicator: function (date) { - /* Prepends an indicator into the chat area, showing the day as - * given by the passed in date. - * - * Parameters: - * (String) date - An ISO8601 date string. - */ - var day_date = moment(date).startOf('day'); - this.$content.prepend(converse.templates.new_day({ - isodate: day_date.format(), - datestring: day_date.format("dddd MMM Do YYYY") - })); - }, - - appendMessage: function (attrs) { - /* Helper method which appends a message to the end of the chat - * box's content area. - * - * Parameters: - * (Object) attrs: An object containing the message attributes. - */ - _.compose( - _.debounce(this.scrollDown.bind(this), 50), - this.$content.append.bind(this.$content) - )(this.renderMessage(attrs)); - }, - - showMessage: function (attrs) { - /* 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. - * - * 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. - */ - var $first_msg = this.$content.children('.chat-message:first'), - first_msg_date = $first_msg.data('isodate'), - last_msg_date, current_msg_date, day_date, $msgs, msg_dates, idx; - if (!first_msg_date) { - this.appendMessage(attrs); - return; - } - current_msg_date = moment(attrs.time) || moment; - last_msg_date = this.$content.children('.chat-message:last').data('isodate'); - - if (typeof last_msg_date !== "undefined" && (current_msg_date.isAfter(last_msg_date) || current_msg_date.isSame(last_msg_date))) { - // The new message is after the last message - if (current_msg_date.isAfter(last_msg_date, 'day')) { - // Append a new day indicator - day_date = moment(current_msg_date).startOf('day'); - this.$content.append(converse.templates.new_day({ - isodate: current_msg_date.format(), - datestring: current_msg_date.format("dddd MMM Do YYYY") - })); - } - this.appendMessage(attrs); - return; - } - - if (typeof first_msg_date !== "undefined" && - (current_msg_date.isBefore(first_msg_date) || - (current_msg_date.isSame(first_msg_date) && !current_msg_date.isSame(last_msg_date)))) { - // The new message is before the first message - - if ($first_msg.prev().length === 0) { - // There's no day indicator before the first message, so we prepend one. - this.prependDayIndicator(first_msg_date); - } - if (current_msg_date.isBefore(first_msg_date, 'day')) { - _.compose( - this.scrollDownMessageHeight.bind(this), - function ($el) { - this.$content.prepend($el); - return $el; - }.bind(this) - )(this.renderMessage(attrs)); - // This message is on a different day, so we add a day indicator. - this.prependDayIndicator(current_msg_date); - } else { - // The message is before the first, but on the same day. - // We need to prepend the message immediately before the - // first message (so that it'll still be after the day indicator). - _.compose( - this.scrollDownMessageHeight.bind(this), - function ($el) { - $el.insertBefore($first_msg); - return $el; - } - )(this.renderMessage(attrs)); - } - } else { - // We need to find the correct place to position the message - current_msg_date = current_msg_date.format(); - $msgs = this.$content.children('.chat-message'); - msg_dates = _.map($msgs, function (el) { - return $(el).data('isodate'); - }); - msg_dates.push(current_msg_date); - msg_dates.sort(); - idx = msg_dates.indexOf(current_msg_date)-1; - _.compose( - this.scrollDownMessageHeight.bind(this), - function ($el) { - $el.insertAfter(this.$content.find('.chat-message[data-isodate="'+msg_dates[idx]+'"]')); - return $el; - }.bind(this) - )(this.renderMessage(attrs)); - } - }, - - renderMessage: function (attrs) { - /* Renders a chat message based on the passed in attributes. - * - * Parameters: - * (Object) attrs: An object containing the message attributes. - * - * Returns: - * The DOM element representing the message. - */ - var msg_time = moment(attrs.time) || moment, - text = attrs.message, - match = text.match(/^\/(.*?)(?: (.*))?$/), - fullname = this.model.get('fullname') || attrs.fullname, - extra_classes = attrs.delayed && 'delayed' || '', - template, username; - - if ((match) && (match[1] === 'me')) { - text = text.replace(/^\/me/, ''); - template = converse.templates.action; - username = fullname; - } else { - template = converse.templates.message; - username = attrs.sender === 'me' && __('me') || fullname; - } - this.$content.find('div.chat-event').remove(); - - // FIXME: leaky abstraction from MUC - if (this.is_chatroom && attrs.sender === 'them' && (new RegExp("\\b"+this.model.get('nick')+"\\b")).test(text)) { - // Add special class to mark groupchat messages in which we - // are mentioned. - extra_classes += ' mentioned'; - } - return $(template({ - msgid: attrs.msgid, - 'sender': attrs.sender, - 'time': msg_time.format('hh:mm'), - 'isodate': msg_time.format(), - 'username': username, - 'message': '', - 'extra_classes': extra_classes - })).children('.chat-msg-content').first().text(text) - .addHyperlinks() - .addEmoticons(converse.visible_toolbar_buttons.emoticons).parent(); - }, - - showHelpMessages: function (msgs, type, spinner) { - var i, msgs_length = msgs.length; - for (i=0; i'+msgs[i]+'')); - } - if (spinner === true) { - this.$content.append(''); - } else if (spinner === false) { - this.$content.find('span.spinner').remove(); - } - return this.scrollDown(); - }, - - handleChatStateMessage: function (message) { - if (message.get('chat_state') === converse.COMPOSING) { - this.showStatusNotification(message.get('fullname')+' '+__('is typing')); - this.clear_status_timeout = window.setTimeout(this.clearStatusNotification.bind(this), 10000); - } else if (message.get('chat_state') === converse.PAUSED) { - this.showStatusNotification(message.get('fullname')+' '+__('has stopped typing')); - } else if (_.contains([converse.INACTIVE, converse.ACTIVE], message.get('chat_state'))) { - this.$content.find('div.chat-event').remove(); - } else if (message.get('chat_state') === converse.GONE) { - this.showStatusNotification(message.get('fullname')+' '+__('has gone away')); - } - }, - - handleTextMessage: function (message) { - this.showMessage(_.clone(message.attributes)); - if ((message.get('sender') !== 'me') && (converse.windowState === 'blur')) { - converse.incrementMsgCounter(); - } - if (!this.model.get('minimized') && !this.$el.is(':visible')) { - this.show(); - } - }, - - onMessageAdded: function (message) { - /* Handler that gets called when a new message object is created. - * - * Parameters: - * (Object) message - The message Backbone object that was added. - */ - if (typeof this.clear_status_timeout !== 'undefined') { - window.clearTimeout(this.clear_status_timeout); - delete this.clear_status_timeout; - } - if (!message.get('message')) { - this.handleChatStateMessage(message); - } else { - this.handleTextMessage(message); - } - }, - - createMessageStanza: function (message) { - return $msg({ - from: converse.connection.jid, - to: this.model.get('jid'), - type: 'chat', - id: message.get('msgid') - }).c('body').t(message.get('message')).up() - .c(converse.ACTIVE, {'xmlns': Strophe.NS.CHATSTATES}).up(); - }, - - sendMessage: function (message) { - /* Responsible for sending off a text message. - * - * Parameters: - * (Message) message - The chat message - */ - // TODO: We might want to send to specfic resources. - // Especially in the OTR case. - var messageStanza = this.createMessageStanza(message); - converse.connection.send(messageStanza); - if (converse.forward_messages) { - // 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:'urn:xmpp:forward:0'}) - .c('delay', {xmns:'urn:xmpp:delay',stamp:(new Date()).getTime()}).up() - .cnode(messageStanza.tree()) - ); - } - }, - - onMessageSubmitted: function (text) { - /* This method gets called once the user has typed a message - * and then pressed enter in a chat box. - * - * Parameters: - * (string) text - The chat message text. - */ - if (!converse.connection.authenticated) { - return this.showHelpMessages( - ['Sorry, the connection has been lost, '+ - 'and your message could not be sent'], - 'error' - ); - } - var match = text.replace(/^\s*/, "").match(/^\/(.*)\s*$/), msgs; - if (match) { - if (match[1] === "clear") { - return this.clearMessages(); - } - else if (match[1] === "help") { - msgs = [ - '/help:'+__('Show this menu')+'', - '/me:'+__('Write in the third person')+'', - '/clear:'+__('Remove messages')+'' - ]; - this.showHelpMessages(msgs); - return; - } - } - var fullname = converse.xmppstatus.get('fullname'); - fullname = _.isEmpty(fullname)? converse.bare_jid: fullname; - var message = this.model.messages.create({ - fullname: fullname, - sender: 'me', - time: moment().format(), - message: text - }); - this.sendMessage(message); - }, - - sendChatState: function () { - /* Sends a message with the status of the user in this chat session - * as taken from the 'chat_state' attribute of the chat box. - * See XEP-0085 Chat State Notifications. - */ - converse.connection.send( - $msg({'to':this.model.get('jid'), 'type': 'chat'}) - .c(this.model.get('chat_state'), {'xmlns': Strophe.NS.CHATSTATES}) - ); - }, - - setChatState: function (state, no_save) { - /* Mutator for setting the chat state of this chat session. - * 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 (typeof this.chat_state_timeout !== 'undefined') { - window.clearTimeout(this.chat_state_timeout); - delete this.chat_state_timeout; - } - if (state === converse.COMPOSING) { - this.chat_state_timeout = window.setTimeout( - this.setChatState.bind(this), converse.TIMEOUTS.PAUSED, converse.PAUSED); - } else if (state === converse.PAUSED) { - this.chat_state_timeout = window.setTimeout( - this.setChatState.bind(this), converse.TIMEOUTS.INACTIVE, converse.INACTIVE); - } - if (!no_save && this.model.get('chat_state') !== state) { - this.model.set('chat_state', state); - } - return this; - }, - - keyPressed: function (ev) { - /* Event handler for when a key is pressed in a chat box textarea. - */ - var $textarea = $(ev.target), message; - if (ev.keyCode === KEY.ENTER) { - ev.preventDefault(); - message = $textarea.val(); - $textarea.val('').focus(); - if (message !== '') { - if (this.model.get('chatroom')) { - this.onChatRoomMessageSubmitted(message); - } else { - this.onMessageSubmitted(message); - } - converse.emit('messageSend', message); - } - this.setChatState(converse.ACTIVE); - } else if (!this.model.get('chatroom')) { // chat state data is currently only for single user chat - // 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); - } - }, - - onStartVerticalResize: function (ev) { - if (!converse.allow_dragresize) { return true; } - // Record element attributes for mouseMove(). - this.height = this.$el.children('.box-flyout').height(); - converse.resizing = { - 'chatbox': this, - 'direction': 'top' - }; - this.prev_pageY = ev.pageY; - }, - - onStartHorizontalResize: function (ev) { - if (!converse.allow_dragresize) { return true; } - this.width = this.$el.children('.box-flyout').width(); - converse.resizing = { - 'chatbox': this, - 'direction': 'left' - }; - this.prev_pageX = ev.pageX; - }, - - onStartDiagonalResize: function (ev) { - this.onStartHorizontalResize(ev); - this.onStartVerticalResize(ev); - converse.resizing.direction = 'topleft'; - }, - - setChatBoxHeight: function (height) { - if (!this.model.get('minimized')) { - if (height) { - height = converse.applyDragResistance(height, this.model.get('default_height'))+'px'; - } else { - height = ""; - } - this.$el.children('.box-flyout')[0].style.height = height; - } - }, - - setChatBoxWidth: function (width) { - if (!this.model.get('minimized')) { - if (width) { - width = converse.applyDragResistance(width, this.model.get('default_width'))+'px'; - } else { - width = ""; - } - this.$el[0].style.width = width; - this.$el.children('.box-flyout')[0].style.width = width; - } - }, - - resizeChatBox: function (ev) { - var diff; - if (converse.resizing.direction.indexOf('top') === 0) { - diff = ev.pageY - this.prev_pageY; - if (diff) { - this.height = ((this.height-diff) > (this.model.get('min_height') || 0)) ? (this.height-diff) : this.model.get('min_height'); - this.prev_pageY = ev.pageY; - this.setChatBoxHeight(this.height); - } - } - if (converse.resizing.direction.indexOf('left') !== -1) { - diff = this.prev_pageX - ev.pageX; - if (diff) { - this.width = ((this.width+diff) > (this.model.get('min_width') || 0)) ? (this.width+diff) : this.model.get('min_width'); - this.prev_pageX = ev.pageX; - this.setChatBoxWidth(this.width); - } - } - }, - - clearMessages: function (ev) { - if (ev && ev.preventDefault) { ev.preventDefault(); } - var result = confirm(__("Are you sure you want to clear the messages from this chat box?")); - if (result === true) { - this.$content.empty(); - this.model.messages.reset(); - this.model.messages.browserStorage._clear(); - } - return this; - }, - - insertEmoticon: function (ev) { - ev.stopPropagation(); - this.$el.find('.toggle-smiley ul').slideToggle(200); - var $textbox = this.$el.find('textarea.chat-textarea'); - var value = $textbox.val(); - var $target = $(ev.target); - $target = $target.is('a') ? $target : $target.children('a'); - if (value && (value[value.length-1] !== ' ')) { - value = value + ' '; - } - $textbox.focus().val(value+$target.data('emoticon')+' '); - }, - - toggleEmoticonMenu: function (ev) { - ev.stopPropagation(); - this.$el.find('.toggle-smiley ul').slideToggle(200); - }, - - toggleCall: function (ev) { - ev.stopPropagation(); - converse.emit('callButtonClicked', { - connection: converse.connection, - model: this.model - }); - }, - - onChatStatusChanged: function (item) { - var chat_status = item.get('chat_status'), - fullname = item.get('fullname'); - fullname = _.isEmpty(fullname)? item.get('jid'): fullname; - if (this.$el.is(':visible')) { - 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') { - this.$el.find('div.chat-event').remove(); - } - } - }, - - onStatusChanged: function (item) { - this.showStatusMessage(); - converse.emit('contactStatusMessageChanged', { - 'contact': item.attributes, - 'message': item.get('status') - }); - }, - - onMinimizedChanged: function (item) { - if (item.get('minimized')) { - this.minimize(); - } else { - this.maximize(); - } - }, - - showStatusMessage: function (msg) { - msg = msg || this.model.get('status'); - if (typeof msg === "string") { - this.$el.find('p.user-custom-message').text(msg).attr('title', msg); - } - return this; - }, - - close: function (ev) { - if (ev && ev.preventDefault) { ev.preventDefault(); } - if (converse.connection.connected) { - this.model.destroy(); - this.setChatState(converse.INACTIVE); - } else { - this.hide(); - } - converse.emit('chatBoxClosed', this); - return this; - }, - - onMaximized: function () { - converse.chatboxviews.trimChats(this); - utils.refreshWebkit(); - this.$content.scrollTop(this.model.get('scroll')); - this.setChatState(converse.ACTIVE).focus(); - converse.emit('chatBoxMaximized', this); - }, - - onMinimized: function () { - utils.refreshWebkit(); - converse.emit('chatBoxMinimized', this); - }, - - maximize: function () { - // Restore a minimized chat box - $('#conversejs').prepend(this.$el); - this.$el.show('fast', this.onMaximized.bind(this)); - return this; - }, - - minimize: function (ev) { - if (ev && ev.preventDefault) { ev.preventDefault(); } - // save the scroll position to restore it on maximize - this.model.save({'scroll': this.$content.scrollTop()}); - this.setChatState(converse.INACTIVE).model.minimize(); - this.$el.hide('fast', this.onMinimized.bind(this)); - }, - - updateVCard: function () { - if (!this.use_vcards) { return this; } - var jid = this.model.get('jid'), - contact = converse.roster.get(jid); - if ((contact) && (!contact.get('vcard_updated'))) { - converse.getVCard( - jid, - function (iq, jid, fullname, image, image_type, url) { - this.model.save({ - 'fullname' : fullname || jid, - 'url': url, - 'image_type': image_type, - 'image': image - }); - }.bind(this), - function () { - converse.log("ChatBoxView.initialize: An error occured while fetching vcard"); - } - ); - } - return this; - }, - - renderToolbar: function (options) { - if (!converse.show_toolbar) { - return; - } - options = _.extend(options || {}, { - label_clear: __('Clear all messages'), - label_hide_occupants: __('Hide the list of occupants'), - 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, - show_emoticons: converse.visible_toolbar_buttons.emoticons, - // FIXME Leaky abstraction MUC - show_occupants_toggle: this.is_chatroom && converse.visible_toolbar_buttons.toggle_occupants - }); - this.$el.find('.chat-toolbar').html(converse.templates.toolbar(_.extend(this.model.toJSON(), options || {}))); - return this; - }, - - renderAvatar: function () { - if (!this.model.get('image')) { - return; - } - var img_src = 'data:'+this.model.get('image_type')+';base64,'+this.model.get('image'), - canvas = $('').get(0); - - if (!(canvas.getContext && canvas.getContext('2d'))) { - return this; - } - var ctx = canvas.getContext('2d'); - var img = new Image(); // Create new Image object - img.onload = function () { - var ratio = img.width/img.height; - if (ratio < 1) { - ctx.drawImage(img, 0,0, 32, 32*(1/ratio)); - } else { - ctx.drawImage(img, 0,0, 32, 32*ratio); - } - - }; - img.src = img_src; - this.$el.find('.chat-title').before(canvas); - return this; - }, - - focus: function () { - this.$el.find('.chat-textarea').focus(); - converse.emit('chatBoxFocused', this); - return this; - }, - - hide: function () { - if (this.$el.is(':visible') && this.$el.css('opacity') === "1") { - this.$el.hide(); - utils.refreshWebkit(); - } - return this; - }, - - show: function (focus) { - if (typeof this.debouncedShow === 'undefined') { - /* 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(function (focus) { - if (this.$el.is(':visible') && this.$el.css('opacity') === "1") { - if (focus) { this.focus(); } - return; - } - this.initDragResize().setDimensions(); - this.$el.fadeIn(function () { - if (converse.connection.connected) { - // Without a connection, we haven't yet initialized - // localstorage - this.model.save(); - } - converse.chatboxviews.trimChats(this); - this.setChatState(converse.ACTIVE); - this.scrollDown(); - if (focus) { - this.focus(); - } - }.bind(this)); - }, 250, true); - } - this.debouncedShow.apply(this, arguments); - return this; - }, - - scrollDownMessageHeight: function ($message) { - if (this.$content.is(':visible')) { - this.$content.scrollTop(this.$content.scrollTop() + $message[0].scrollHeight); - } - return this; - }, - - scrollDown: function () { - if (this.$content.is(':visible')) { - this.$content.scrollTop(this.$content[0].scrollHeight); - } - return this; - } - }); this.ChatBoxes = Backbone.Collection.extend({ model: converse.ChatBox, @@ -2443,13 +1566,11 @@ onChatBoxAdded: function (item) { var view = this.get(item.get('id')); - if (!view) { - view = new converse.ChatBoxView({model: item}); - this.add(item.get('id'), view); - } else { + if (view) { delete view.model; // Remove ref to old model to help garbage collection view.model = item; view.initialize(); + this.trimChats(view); } }, @@ -3134,8 +2255,20 @@ }; this.initializePlugins = function () { + + var updateSettings = function (settings) { + /* Helper method which gets put on the plugin and allows it to + * add more user-facing config settings to converse.js. + */ + _.extend(converse.default_settings, settings); + _.extend(converse, settings); + _.extend(converse, _.pick(converse.user_settings, Object.keys(settings))); + }; + _.each(_.keys(this.plugins), function (name) { var plugin = this.plugins[name]; + plugin.updateSettings = updateSettings; + if (_.contains(this.initialized_plugins, name)) { // Don't initialize plugins twice, otherwise we get // infinite recursion in overridden methods. diff --git a/src/converse-headline.js b/src/converse-headline.js index 56336a2bf..0ed5ab7af 100644 --- a/src/converse-headline.js +++ b/src/converse-headline.js @@ -7,18 +7,17 @@ /*global define */ (function (root, factory) { - define("converse-headline", ["converse-core", "converse-api"], factory); + define("converse-headline", [ + "converse-core", + "converse-api", + // TODO: remove this dependency + "converse-chat" + ], factory); }(this, function (converse, converse_api) { "use strict"; - var $ = converse_api.env.jQuery, - utils = converse_api.env.utils, - Strophe = converse_api.env.Strophe, - _ = converse_api.env._; - // For translations - var __ = utils.__.bind(converse); - var ___ = utils.___; - - var supports_html5_notification = "Notification" in window; + var utils = converse_api.env.utils, + _ = converse_api.env._, + __ = utils.__.bind(converse); converse_api.plugins.add('headline', { diff --git a/src/converse-muc.js b/src/converse-muc.js index 0ce7d507b..331a3dd50 100755 --- a/src/converse-muc.js +++ b/src/converse-muc.js @@ -13,6 +13,8 @@ define("converse-muc", [ "converse-core", "converse-api", + // TODO remove next two dependencies + "converse-chatview", "converse-controlbox" ], factory); }(this, function (converse, converse_api) { @@ -202,15 +204,14 @@ */ var converse = this.converse; // Configuration values for this plugin - var settings = { + this.updateSettings({ allow_muc: true, auto_join_on_invite: false, // Auto-join chatroom on invite hide_muc_server: false, muc_history_max_stanzas: undefined, // Takes an integer, limits the amount of messages to fetch from chat room's history - }; - _.extend(converse.default_settings, settings); - _.extend(converse, settings); - _.extend(converse, _.pick(converse.user_settings, Object.keys(settings))); + show_toolbar: true, + }); + converse.ChatRoomView = converse.ChatBoxView.extend({ /* Backbone View which renders a chat room, based upon the view