From: Mikael Nordfeldth Date: Sun, 23 Feb 2014 23:59:29 +0000 (+0100) Subject: Preparing plugins for no-minify-in-core-policy X-Git-Url: https://git.mxchange.org/?a=commitdiff_plain;h=97830b07019d9ffe33e2c2048bac39026e636998;p=quix0rs-gnu-social.git Preparing plugins for no-minify-in-core-policy also making the file structure better with js and css folders for Realtime and LinkPreview --- diff --git a/plugins/Comet/CometPlugin.php b/plugins/Comet/CometPlugin.php index 70f5ab85fe..22f791e2fc 100644 --- a/plugins/Comet/CometPlugin.php +++ b/plugins/Comet/CometPlugin.php @@ -27,12 +27,10 @@ * @link http://status.net/ */ -if (!defined('STATUSNET') && !defined('LACONICA')) { +if (!defined('GNUSOCIAL') && !defined('STATUSNET')) { exit(1); } -require_once INSTALLDIR.'/plugins/Realtime/RealtimePlugin.php'; - /** * Plugin to do realtime updates using Comet * @@ -64,10 +62,10 @@ class CometPlugin extends RealtimePlugin { $scripts = parent::_getScripts(); - $ours = array('jquery.comet.js', 'cometupdate.js'); + $ours = array('js/jquery.comet.js', 'js/cometupdate.js'); foreach ($ours as $script) { - $scripts[] = 'plugins/Comet/'.$script; + $scripts[] = $this->path($script); } return $scripts; @@ -81,7 +79,7 @@ class CometPlugin extends RealtimePlugin function _connect() { - require_once INSTALLDIR.'/plugins/Comet/bayeux.class.inc.php'; + require_once INSTALLDIR.'/plugins/Comet/extlib/Bayeux/Bayeux.class.php'; // Bayeux? Comet? Huh? These terms confuse me $this->bay = new Bayeux($this->server, $this->user, $this->password); } diff --git a/plugins/Comet/bayeux.class.inc.php b/plugins/Comet/bayeux.class.inc.php deleted file mode 100644 index 4fe8600f84..0000000000 --- a/plugins/Comet/bayeux.class.inc.php +++ /dev/null @@ -1,133 +0,0 @@ - http://morglog.alleycatracing.com - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA - * - */ - -class Bayeux -{ - private $oCurl = ''; - private $nNextId = 0; - - private $sUser = ''; - private $sPassword = ''; - - public $sUrl = ''; - - function __construct($sUrl, $sUser='', $sPassword='') - { - $this->sUrl = $sUrl; - - $this->oCurl = curl_init(); - - $aHeaders = array(); - $aHeaders[] = 'Connection: Keep-Alive'; - - curl_setopt($this->oCurl, CURLOPT_URL, $sUrl); - curl_setopt($this->oCurl, CURLOPT_HTTPHEADER, $aHeaders); - curl_setopt($this->oCurl, CURLOPT_HEADER, 0); - curl_setopt($this->oCurl, CURLOPT_POST, 1); - curl_setopt($this->oCurl, CURLOPT_RETURNTRANSFER,1); - - if (!is_null($sUser) && mb_strlen($sUser) > 0) { - curl_setopt($this->oCurl, CURLOPT_USERPWD,"$sUser:$sPassword"); - } - - $this->handShake(); - } - - function __destruct() - { - $this->disconnect(); - } - - function handShake() - { - $msgHandshake = array(); - $msgHandshake['channel'] = '/meta/handshake'; - $msgHandshake['version'] = "1.0"; - $msgHandshake['minimumVersion'] = "0.9"; - $msgHandshake['supportedConnectionTypes'] = array('long-polling'); - $msgHandshake['id'] = $this->nNextId++; - - curl_setopt($this->oCurl, CURLOPT_POSTFIELDS, "message=".urlencode(str_replace('\\', '', json_encode(array($msgHandshake))))); - - $data = curl_exec($this->oCurl); - - if(curl_errno($this->oCurl)) - die("Error: " . curl_error($this->oCurl)); - - $oReturn = json_decode($data); - - if (is_array($oReturn)) { - $oReturn = $oReturn[0]; - } - - $bSuccessful = ($oReturn->successful) ? true : false; - - if($bSuccessful) - { - $this->clientId = $oReturn->clientId; - - $this->connect(); - } - } - - public function connect() - { - $aMsg['channel'] = '/meta/connect'; - $aMsg['id'] = $this->nNextId++; - $aMsg['clientId'] = $this->clientId; - $aMsg['connectionType'] = 'long-polling'; - - curl_setopt($this->oCurl, CURLOPT_POSTFIELDS, "message=".urlencode(str_replace('\\', '', json_encode(array($aMsg))))); - - $data = curl_exec($this->oCurl); - } - - function disconnect() - { - $msgHandshake = array(); - $msgHandshake['channel'] = '/meta/disconnect'; - $msgHandshake['id'] = $this->nNextId++; - $msgHandshake['clientId'] = $this->clientId; - - curl_setopt($this->oCurl, CURLOPT_POSTFIELDS, "message=".urlencode(str_replace('\\', '', json_encode(array($msgHandshake))))); - - curl_exec($this->oCurl); - } - - public function publish($sChannel, $oData) - { - if(!$sChannel || !$oData) - return; - - $aMsg = array(); - - $aMsg['channel'] = $sChannel; - $aMsg['id'] = $this->nNextId++; - $aMsg['data'] = $oData; - $aMsg['clientId'] = $this->clientId; - - curl_setopt($this->oCurl, CURLOPT_POSTFIELDS, "message=".urlencode(str_replace('\\', '', json_encode(array($aMsg))))); - - $data = curl_exec($this->oCurl); -// var_dump($data); - } -} diff --git a/plugins/Comet/cometupdate.js b/plugins/Comet/cometupdate.js deleted file mode 100644 index 50b02b7f34..0000000000 --- a/plugins/Comet/cometupdate.js +++ /dev/null @@ -1,27 +0,0 @@ -// update the local timeline from a Comet server -var CometUpdate = function() -{ - var _server; - var _timeline; - var _userid; - var _replyurl; - var _favorurl; - var _deleteurl; - var _cometd; - - return { - init: function(server, timeline, userid, replyurl, favorurl, deleteurl) - { - _cometd = $.cometd; // Uses the default Comet object - _cometd.init(server); - _server = server; - _timeline = timeline; - _userid = userid; - _favorurl = favorurl; - _replyurl = replyurl; - _deleteurl = deleteurl; - _cometd.subscribe(timeline, function(message) { RealtimeUpdate.receive(message.data) }); - $(window).unload(function() { _cometd.disconnect(); } ); - } - } -}(); diff --git a/plugins/Comet/extlib/Bayeux/Bayeux.class.php b/plugins/Comet/extlib/Bayeux/Bayeux.class.php new file mode 100644 index 0000000000..4fe8600f84 --- /dev/null +++ b/plugins/Comet/extlib/Bayeux/Bayeux.class.php @@ -0,0 +1,133 @@ + http://morglog.alleycatracing.com + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ + +class Bayeux +{ + private $oCurl = ''; + private $nNextId = 0; + + private $sUser = ''; + private $sPassword = ''; + + public $sUrl = ''; + + function __construct($sUrl, $sUser='', $sPassword='') + { + $this->sUrl = $sUrl; + + $this->oCurl = curl_init(); + + $aHeaders = array(); + $aHeaders[] = 'Connection: Keep-Alive'; + + curl_setopt($this->oCurl, CURLOPT_URL, $sUrl); + curl_setopt($this->oCurl, CURLOPT_HTTPHEADER, $aHeaders); + curl_setopt($this->oCurl, CURLOPT_HEADER, 0); + curl_setopt($this->oCurl, CURLOPT_POST, 1); + curl_setopt($this->oCurl, CURLOPT_RETURNTRANSFER,1); + + if (!is_null($sUser) && mb_strlen($sUser) > 0) { + curl_setopt($this->oCurl, CURLOPT_USERPWD,"$sUser:$sPassword"); + } + + $this->handShake(); + } + + function __destruct() + { + $this->disconnect(); + } + + function handShake() + { + $msgHandshake = array(); + $msgHandshake['channel'] = '/meta/handshake'; + $msgHandshake['version'] = "1.0"; + $msgHandshake['minimumVersion'] = "0.9"; + $msgHandshake['supportedConnectionTypes'] = array('long-polling'); + $msgHandshake['id'] = $this->nNextId++; + + curl_setopt($this->oCurl, CURLOPT_POSTFIELDS, "message=".urlencode(str_replace('\\', '', json_encode(array($msgHandshake))))); + + $data = curl_exec($this->oCurl); + + if(curl_errno($this->oCurl)) + die("Error: " . curl_error($this->oCurl)); + + $oReturn = json_decode($data); + + if (is_array($oReturn)) { + $oReturn = $oReturn[0]; + } + + $bSuccessful = ($oReturn->successful) ? true : false; + + if($bSuccessful) + { + $this->clientId = $oReturn->clientId; + + $this->connect(); + } + } + + public function connect() + { + $aMsg['channel'] = '/meta/connect'; + $aMsg['id'] = $this->nNextId++; + $aMsg['clientId'] = $this->clientId; + $aMsg['connectionType'] = 'long-polling'; + + curl_setopt($this->oCurl, CURLOPT_POSTFIELDS, "message=".urlencode(str_replace('\\', '', json_encode(array($aMsg))))); + + $data = curl_exec($this->oCurl); + } + + function disconnect() + { + $msgHandshake = array(); + $msgHandshake['channel'] = '/meta/disconnect'; + $msgHandshake['id'] = $this->nNextId++; + $msgHandshake['clientId'] = $this->clientId; + + curl_setopt($this->oCurl, CURLOPT_POSTFIELDS, "message=".urlencode(str_replace('\\', '', json_encode(array($msgHandshake))))); + + curl_exec($this->oCurl); + } + + public function publish($sChannel, $oData) + { + if(!$sChannel || !$oData) + return; + + $aMsg = array(); + + $aMsg['channel'] = $sChannel; + $aMsg['id'] = $this->nNextId++; + $aMsg['data'] = $oData; + $aMsg['clientId'] = $this->clientId; + + curl_setopt($this->oCurl, CURLOPT_POSTFIELDS, "message=".urlencode(str_replace('\\', '', json_encode(array($aMsg))))); + + $data = curl_exec($this->oCurl); +// var_dump($data); + } +} diff --git a/plugins/Comet/jquery.comet.js b/plugins/Comet/jquery.comet.js deleted file mode 100644 index 6de437fa8e..0000000000 --- a/plugins/Comet/jquery.comet.js +++ /dev/null @@ -1,1451 +0,0 @@ -/** - * Copyright 2008 Mort Bay Consulting Pty. Ltd. - * Dual licensed under the Apache License 2.0 and the MIT license. - * ---------------------------------------------------------------------------- - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * http: *www.apache.org/licenses/LICENSE-2.0 - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * ---------------------------------------------------------------------------- - * Licensed under the MIT license; - * Permission is hereby granted, free of charge, to any person obtaining - * a copy of this software and associated documentation files (the - * "Software"), to deal in the Software without restriction, including - * without limitation the rights to use, copy, modify, merge, publish, - * distribute, sublicense, and/or sell copies of the Software, and to - * permit persons to whom the Software is furnished to do so, subject to - * the following conditions: - * - * The above copyright notice and this permission notice shall be - * included in all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE - * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION - * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - * ---------------------------------------------------------------------------- - * $Revision$ $Date$ - */ -(function($) -{ - /** - * The constructor for a Comet object. - * There is a default Comet instance already created at the variable $.cometd, - * and hence that can be used to start a comet conversation with a server. - * In the rare case a page needs more than one comet conversation, a new instance can be - * created via: - *
-     * var url2 = ...;
-     * var cometd2 = new $.Cometd();
-     * cometd2.init(url2);
-     * 
- */ - $.Cometd = function(name) - { - var _name = name || 'default'; - var _logPriorities = { debug: 1, info: 2, warn: 3, error: 4 }; - var _logLevel = 'info'; - var _url; - var _xd = false; - var _transport; - var _status = 'disconnected'; - var _messageId = 0; - var _clientId = null; - var _batch = 0; - var _messageQueue = []; - var _listeners = {}; - var _backoff = 0; - var _backoffIncrement = 1000; - var _maxBackoff = 60000; - var _scheduledSend = null; - var _extensions = []; - var _advice = {}; - var _handshakeProps; - - /** - * Returns the name assigned to this Comet object, or the string 'default' - * if no name has been explicitely passed as parameter to the constructor. - */ - this.getName = function() - { - return _name; - }; - - /** - * Configures the initial comet communication with the comet server. - * @param cometURL the URL of the comet server - */ - this.configure = function(cometURL) - { - _configure(cometURL); - }; - - function _configure(cometURL) - { - _url = cometURL; - _debug('Initializing comet with url: {}', _url); - - // Check immediately if we're cross domain - // If cross domain, the handshake must not send the long polling transport type - var urlParts = /(^https?:)?(\/\/(([^:\/\?#]+)(:(\d+))?))?([^\?#]*)/.exec(cometURL); - if (urlParts[3]) _xd = urlParts[3] != location.host; - - // Temporary setup a transport to send the initial handshake - // The transport may be changed as a result of handshake - if (_xd) - _transport = newCallbackPollingTransport(); - else - _transport = newLongPollingTransport(); - _debug('Initial transport is {}', _transport.getType()); - }; - - /** - * Configures and establishes the comet communication with the comet server - * via a handshake and a subsequent connect. - * @param cometURL the URL of the comet server - * @param handshakeProps an object to be merged with the handshake message - * @see #configure(cometURL) - * @see #handshake(handshakeProps) - */ - this.init = function(cometURL, handshakeProps) - { - _configure(cometURL); - _handshake(handshakeProps); - }; - - /** - * Establishes the comet communication with the comet server - * via a handshake and a subsequent connect. - * @param handshakeProps an object to be merged with the handshake message - */ - this.handshake = function(handshakeProps) - { - _handshake(handshakeProps); - }; - - /** - * Disconnects from the comet server. - * @param disconnectProps an object to be merged with the disconnect message - */ - this.disconnect = function(disconnectProps) - { - var bayeuxMessage = { - channel: '/meta/disconnect' - }; - var message = $.extend({}, disconnectProps, bayeuxMessage); - // Deliver immediately - // The handshake and connect mechanism make use of startBatch(), and in case - // of a failed handshake the disconnect would not be delivered if using _send(). - _setStatus('disconnecting'); - _deliver([message], false); - }; - - /** - * Marks the start of a batch of application messages to be sent to the server - * in a single request, obtaining a single response containing (possibly) many - * application reply messages. - * Messages are held in a queue and not sent until {@link #endBatch()} is called. - * If startBatch() is called multiple times, then an equal number of endBatch() - * calls must be made to close and send the batch of messages. - * @see #endBatch() - */ - this.startBatch = function() - { - _startBatch(); - }; - - /** - * Marks the end of a batch of application messages to be sent to the server - * in a single request. - * @see #startBatch() - */ - this.endBatch = function() - { - _endBatch(true); - }; - - /** - * Subscribes to the given channel, performing the given callback in the given scope - * when a message for the channel arrives. - * @param channel the channel to subscribe to - * @param scope the scope of the callback - * @param callback the callback to call when a message is delivered to the channel - * @param subscribeProps an object to be merged with the subscribe message - * @return the subscription handle to be passed to {@link #unsubscribe(object)} - */ - this.subscribe = function(channel, scope, callback, subscribeProps) - { - var subscription = this.addListener(channel, scope, callback); - - // Send the subscription message after the subscription registration to avoid - // races where the server would deliver a message to the subscribers, but here - // on the client the subscription has not been added yet to the data structures - var bayeuxMessage = { - channel: '/meta/subscribe', - subscription: channel - }; - var message = $.extend({}, subscribeProps, bayeuxMessage); - _send(message); - - return subscription; - }; - - /** - * Unsubscribes the subscription obtained with a call to {@link #subscribe(string, object, function)}. - * @param subscription the subscription to unsubscribe. - */ - this.unsubscribe = function(subscription, unsubscribeProps) - { - // Remove the local listener before sending the message - // This ensures that if the server fails, this client does not get notifications - this.removeListener(subscription); - var bayeuxMessage = { - channel: '/meta/unsubscribe', - subscription: subscription[0] - }; - var message = $.extend({}, unsubscribeProps, bayeuxMessage); - _send(message); - }; - - /** - * Publishes a message on the given channel, containing the given content. - * @param channel the channel to publish the message to - * @param content the content of the message - * @param publishProps an object to be merged with the publish message - */ - this.publish = function(channel, content, publishProps) - { - var bayeuxMessage = { - channel: channel, - data: content - }; - var message = $.extend({}, publishProps, bayeuxMessage); - _send(message); - }; - - /** - * Adds a listener for bayeux messages, performing the given callback in the given scope - * when a message for the given channel arrives. - * @param channel the channel the listener is interested to - * @param scope the scope of the callback - * @param callback the callback to call when a message is delivered to the channel - * @returns the subscription handle to be passed to {@link #removeListener(object)} - * @see #removeListener(object) - */ - this.addListener = function(channel, scope, callback) - { - // The data structure is a map, where each subscription - // holds the callback to be called and its scope. - - // Normalize arguments - if (!callback) - { - callback = scope; - scope = undefined; - } - - var subscription = { - scope: scope, - callback: callback - }; - - var subscriptions = _listeners[channel]; - if (!subscriptions) - { - subscriptions = []; - _listeners[channel] = subscriptions; - } - // Pushing onto an array appends at the end and returns the id associated with the element increased by 1. - // Note that if: - // a.push('a'); var hb=a.push('b'); delete a[hb-1]; var hc=a.push('c'); - // then: - // hc==3, a.join()=='a',,'c', a.length==3 - var subscriptionIndex = subscriptions.push(subscription) - 1; - _debug('Added listener: channel \'{}\', callback \'{}\', index {}', channel, callback.name, subscriptionIndex); - - // The subscription to allow removal of the listener is made of the channel and the index - return [channel, subscriptionIndex]; - }; - - /** - * Removes the subscription obtained with a call to {@link #addListener(string, object, function)}. - * @param subscription the subscription to unsubscribe. - */ - this.removeListener = function(subscription) - { - var subscriptions = _listeners[subscription[0]]; - if (subscriptions) - { - delete subscriptions[subscription[1]]; - _debug('Removed listener: channel \'{}\', index {}', subscription[0], subscription[1]); - } - }; - - /** - * Removes all listeners registered with {@link #addListener(channel, scope, callback)} or - * {@link #subscribe(channel, scope, callback)}. - */ - this.clearListeners = function() - { - _listeners = {}; - }; - - /** - * Returns a string representing the status of the bayeux communication with the comet server. - */ - this.getStatus = function() - { - return _status; - }; - - /** - * Sets the backoff period used to increase the backoff time when retrying an unsuccessful or failed message. - * Default value is 1 second, which means if there is a persistent failure the retries will happen - * after 1 second, then after 2 seconds, then after 3 seconds, etc. So for example with 15 seconds of - * elapsed time, there will be 5 retries (at 1, 3, 6, 10 and 15 seconds elapsed). - * @param period the backoff period to set - * @see #getBackoffIncrement() - */ - this.setBackoffIncrement = function(period) - { - _backoffIncrement = period; - }; - - /** - * Returns the backoff period used to increase the backoff time when retrying an unsuccessful or failed message. - * @see #setBackoffIncrement(period) - */ - this.getBackoffIncrement = function() - { - return _backoffIncrement; - }; - - /** - * Returns the backoff period to wait before retrying an unsuccessful or failed message. - */ - this.getBackoffPeriod = function() - { - return _backoff; - }; - - /** - * Sets the log level for console logging. - * Valid values are the strings 'error', 'warn', 'info' and 'debug', from - * less verbose to more verbose. - * @param level the log level string - */ - this.setLogLevel = function(level) - { - _logLevel = level; - }; - - /** - * Registers an extension whose callbacks are called for every incoming message - * (that comes from the server to this client implementation) and for every - * outgoing message (that originates from this client implementation for the - * server). - * The format of the extension object is the following: - *
-         * {
-         *     incoming: function(message) { ... },
-         *     outgoing: function(message) { ... }
-         * }
-         * Both properties are optional, but if they are present they will be called
-         * respectively for each incoming message and for each outgoing message.
-         * 
- * @param name the name of the extension - * @param extension the extension to register - * @return true if the extension was registered, false otherwise - * @see #unregisterExtension(name) - */ - this.registerExtension = function(name, extension) - { - var existing = false; - for (var i = 0; i < _extensions.length; ++i) - { - var existingExtension = _extensions[i]; - if (existingExtension.name == name) - { - existing = true; - return false; - } - } - if (!existing) - { - _extensions.push({ - name: name, - extension: extension - }); - _debug('Registered extension \'{}\'', name); - return true; - } - else - { - _info('Could not register extension with name \'{}\': another extension with the same name already exists'); - return false; - } - }; - - /** - * Unregister an extension previously registered with - * {@link #registerExtension(name, extension)}. - * @param name the name of the extension to unregister. - * @return true if the extension was unregistered, false otherwise - */ - this.unregisterExtension = function(name) - { - var unregistered = false; - $.each(_extensions, function(index, extension) - { - if (extension.name == name) - { - _extensions.splice(index, 1); - unregistered = true; - _debug('Unregistered extension \'{}\'', name); - return false; - } - }); - return unregistered; - }; - - /** - * Starts a the batch of messages to be sent in a single request. - * @see _endBatch(deliverMessages) - */ - function _startBatch() - { - ++_batch; - }; - - /** - * Ends the batch of messages to be sent in a single request, - * optionally delivering messages present in the message queue depending - * on the given argument. - * @param deliverMessages whether to deliver the messages in the queue or not - * @see _startBatch() - */ - function _endBatch(deliverMessages) - { - --_batch; - if (_batch < 0) _batch = 0; - if (deliverMessages && _batch == 0 && !_isDisconnected()) - { - var messages = _messageQueue; - _messageQueue = []; - if (messages.length > 0) _deliver(messages, false); - } - }; - - function _nextMessageId() - { - return ++_messageId; - }; - - /** - * Converts the given response into an array of bayeux messages - * @param response the response to convert - * @return an array of bayeux messages obtained by converting the response - */ - function _convertToMessages(response) - { - if (response === undefined) return []; - if (response instanceof Array) return response; - if (response instanceof String || typeof response == 'string') return eval('(' + response + ')'); - if (response instanceof Object) return [response]; - throw 'Conversion Error ' + response + ', typeof ' + (typeof response); - }; - - function _setStatus(newStatus) - { - _debug('{} -> {}', _status, newStatus); - _status = newStatus; - }; - - function _isDisconnected() - { - return _status == 'disconnecting' || _status == 'disconnected'; - }; - - /** - * Sends the initial handshake message - */ - function _handshake(handshakeProps) - { - _debug('Starting handshake'); - _clientId = null; - - // Start a batch. - // This is needed because handshake and connect are async. - // It may happen that the application calls init() then subscribe() - // and the subscribe message is sent before the connect message, if - // the subscribe message is not held until the connect message is sent. - // So here we start a batch to hold temporarly any message until - // the connection is fully established. - _batch = 0; - _startBatch(); - - // Save the original properties provided by the user - // Deep copy to avoid the user to be able to change them later - _handshakeProps = $.extend(true, {}, handshakeProps); - - var bayeuxMessage = { - version: '1.0', - minimumVersion: '0.9', - channel: '/meta/handshake', - supportedConnectionTypes: _xd ? ['callback-polling'] : ['long-polling', 'callback-polling'] - }; - // Do not allow the user to mess with the required properties, - // so merge first the user properties and *then* the bayeux message - var message = $.extend({}, handshakeProps, bayeuxMessage); - - // We started a batch to hold the application messages, - // so here we must bypass it and deliver immediately. - _setStatus('handshaking'); - _deliver([message], false); - }; - - function _findTransport(handshakeResponse) - { - var transportTypes = handshakeResponse.supportedConnectionTypes; - if (_xd) - { - // If we are cross domain, check if the server supports it, that's the only option - if ($.inArray('callback-polling', transportTypes) >= 0) return _transport; - } - else - { - // Check if we can keep long-polling - if ($.inArray('long-polling', transportTypes) >= 0) return _transport; - - // The server does not support long-polling - if ($.inArray('callback-polling', transportTypes) >= 0) return newCallbackPollingTransport(); - } - return null; - }; - - function _delayedHandshake() - { - _setStatus('handshaking'); - _delayedSend(function() - { - _handshake(_handshakeProps); - }); - }; - - function _delayedConnect() - { - _setStatus('connecting'); - _delayedSend(function() - { - _connect(); - }); - }; - - function _delayedSend(operation) - { - _cancelDelayedSend(); - var delay = _backoff; - _debug("Delayed send: backoff {}, interval {}", _backoff, _advice.interval); - if (_advice.interval && _advice.interval > 0) - delay += _advice.interval; - _scheduledSend = _setTimeout(operation, delay); - }; - - function _cancelDelayedSend() - { - if (_scheduledSend !== null) clearTimeout(_scheduledSend); - _scheduledSend = null; - }; - - function _setTimeout(funktion, delay) - { - return setTimeout(function() - { - try - { - funktion(); - } - catch (x) - { - _debug('Exception during scheduled execution of function \'{}\': {}', funktion.name, x); - } - }, delay); - }; - - /** - * Sends the connect message - */ - function _connect() - { - _debug('Starting connect'); - var message = { - channel: '/meta/connect', - connectionType: _transport.getType() - }; - _setStatus('connecting'); - _deliver([message], true); - _setStatus('connected'); - }; - - function _send(message) - { - if (_batch > 0) - _messageQueue.push(message); - else - _deliver([message], false); - }; - - /** - * Delivers the messages to the comet server - * @param messages the array of messages to send - */ - function _deliver(messages, comet) - { - // We must be sure that the messages have a clientId. - // This is not guaranteed since the handshake may take time to return - // (and hence the clientId is not known yet) and the application - // may create other messages. - $.each(messages, function(index, message) - { - message['id'] = _nextMessageId(); - if (_clientId) message['clientId'] = _clientId; - messages[index] = _applyOutgoingExtensions(message); - }); - - var self = this; - var envelope = { - url: _url, - messages: messages, - onSuccess: function(request, response) - { - try - { - _handleSuccess.call(self, request, response, comet); - } - catch (x) - { - _debug('Exception during execution of success callback: {}', x); - } - }, - onFailure: function(request, reason, exception) - { - try - { - _handleFailure.call(self, request, messages, reason, exception, comet); - } - catch (x) - { - _debug('Exception during execution of failure callback: {}', x); - } - } - }; - _debug('Sending request to {}, message(s): {}', envelope.url, JSON.stringify(envelope.messages)); - _transport.send(envelope, comet); - }; - - function _applyIncomingExtensions(message) - { - for (var i = 0; i < _extensions.length; ++i) - { - var extension = _extensions[i]; - var callback = extension.extension.incoming; - if (callback && typeof callback === 'function') - { - _debug('Calling incoming extension \'{}\', callback \'{}\'', extension.name, callback.name); - message = _applyExtension(extension.name, callback, message) || message; - } - } - return message; - }; - - function _applyOutgoingExtensions(message) - { - for (var i = 0; i < _extensions.length; ++i) - { - var extension = _extensions[i]; - var callback = extension.extension.outgoing; - if (callback && typeof callback === 'function') - { - _debug('Calling outgoing extension \'{}\', callback \'{}\'', extension.name, callback.name); - message = _applyExtension(extension.name, callback, message) || message; - } - } - return message; - }; - - function _applyExtension(name, callback, message) - { - try - { - return callback(message); - } - catch (x) - { - _debug('Exception during execution of extension \'{}\': {}', name, x); - return message; - } - }; - - function _handleSuccess(request, response, comet) - { - var messages = _convertToMessages(response); - _debug('Received response {}', JSON.stringify(messages)); - - // Signal the transport it can deliver other queued requests - _transport.complete(request, true, comet); - - for (var i = 0; i < messages.length; ++i) - { - var message = messages[i]; - message = _applyIncomingExtensions(message); - - if (message.advice) _advice = message.advice; - - var channel = message.channel; - switch (channel) - { - case '/meta/handshake': - _handshakeSuccess(message); - break; - case '/meta/connect': - _connectSuccess(message); - break; - case '/meta/disconnect': - _disconnectSuccess(message); - break; - case '/meta/subscribe': - _subscribeSuccess(message); - break; - case '/meta/unsubscribe': - _unsubscribeSuccess(message); - break; - default: - _messageSuccess(message); - break; - } - } - }; - - function _handleFailure(request, messages, reason, exception, comet) - { - var xhr = request.xhr; - _debug('Request failed, status: {}, reason: {}, exception: {}', xhr && xhr.status, reason, exception); - - // Signal the transport it can deliver other queued requests - _transport.complete(request, false, comet); - - for (var i = 0; i < messages.length; ++i) - { - var message = messages[i]; - var channel = message.channel; - switch (channel) - { - case '/meta/handshake': - _handshakeFailure(xhr, message); - break; - case '/meta/connect': - _connectFailure(xhr, message); - break; - case '/meta/disconnect': - _disconnectFailure(xhr, message); - break; - case '/meta/subscribe': - _subscribeFailure(xhr, message); - break; - case '/meta/unsubscribe': - _unsubscribeFailure(xhr, message); - break; - default: - _messageFailure(xhr, message); - break; - } - } - }; - - function _handshakeSuccess(message) - { - if (message.successful) - { - _debug('Handshake successful'); - // Save clientId, figure out transport, then follow the advice to connect - _clientId = message.clientId; - - var newTransport = _findTransport(message); - if (newTransport === null) - { - throw 'Could not agree on transport with server'; - } - else - { - if (_transport.getType() != newTransport.getType()) - { - _debug('Changing transport from {} to {}', _transport.getType(), newTransport.getType()); - _transport = newTransport; - } - } - - // Notify the listeners - // Here the new transport is in place, as well as the clientId, so - // the listener can perform a publish() if it wants, and the listeners - // are notified before the connect below. - _notifyListeners('/meta/handshake', message); - - var action = _advice.reconnect ? _advice.reconnect : 'retry'; - switch (action) - { - case 'retry': - _delayedConnect(); - break; - default: - break; - } - } - else - { - _debug('Handshake unsuccessful'); - - var retry = !_isDisconnected() && _advice.reconnect != 'none'; - if (!retry) _setStatus('disconnected'); - - _notifyListeners('/meta/handshake', message); - _notifyListeners('/meta/unsuccessful', message); - - // Only try again if we haven't been disconnected and - // the advice permits us to retry the handshake - if (retry) - { - _increaseBackoff(); - _debug('Handshake failure, backing off and retrying in {} ms', _backoff); - _delayedHandshake(); - } - } - }; - - function _handshakeFailure(xhr, message) - { - _debug('Handshake failure'); - - // Notify listeners - var failureMessage = { - successful: false, - failure: true, - channel: '/meta/handshake', - request: message, - xhr: xhr, - advice: { - action: 'retry', - interval: _backoff - } - }; - - var retry = !_isDisconnected() && _advice.reconnect != 'none'; - if (!retry) _setStatus('disconnected'); - - _notifyListeners('/meta/handshake', failureMessage); - _notifyListeners('/meta/unsuccessful', failureMessage); - - // Only try again if we haven't been disconnected and the - // advice permits us to try again - if (retry) - { - _increaseBackoff(); - _debug('Handshake failure, backing off and retrying in {} ms', _backoff); - _delayedHandshake(); - } - }; - - function _connectSuccess(message) - { - var action = _isDisconnected() ? 'none' : (_advice.reconnect ? _advice.reconnect : 'retry'); - if (!_isDisconnected()) _setStatus(action == 'retry' ? 'connecting' : 'disconnecting'); - - if (message.successful) - { - _debug('Connect successful'); - - // End the batch and allow held messages from the application - // to go to the server (see _handshake() where we start the batch). - // The batch is ended before notifying the listeners, so that - // listeners can batch other cometd operations - _endBatch(true); - - // Notify the listeners after the status change but before the next connect - _notifyListeners('/meta/connect', message); - - // Connect was successful. - // Normally, the advice will say "reconnect: 'retry', interval: 0" - // and the server will hold the request, so when a response returns - // we immediately call the server again (long polling) - switch (action) - { - case 'retry': - _resetBackoff(); - _delayedConnect(); - break; - default: - _resetBackoff(); - _setStatus('disconnected'); - break; - } - } - else - { - _debug('Connect unsuccessful'); - - // Notify the listeners after the status change but before the next action - _notifyListeners('/meta/connect', message); - _notifyListeners('/meta/unsuccessful', message); - - // Connect was not successful. - // This may happen when the server crashed, the current clientId - // will be invalid, and the server will ask to handshake again - switch (action) - { - case 'retry': - _increaseBackoff(); - _delayedConnect(); - break; - case 'handshake': - // End the batch but do not deliver the messages until we connect successfully - _endBatch(false); - _resetBackoff(); - _delayedHandshake(); - break; - case 'none': - _resetBackoff(); - _setStatus('disconnected'); - break; - } - } - }; - - function _connectFailure(xhr, message) - { - _debug('Connect failure'); - - // Notify listeners - var failureMessage = { - successful: false, - failure: true, - channel: '/meta/connect', - request: message, - xhr: xhr, - advice: { - action: 'retry', - interval: _backoff - } - }; - _notifyListeners('/meta/connect', failureMessage); - _notifyListeners('/meta/unsuccessful', failureMessage); - - if (!_isDisconnected()) - { - var action = _advice.reconnect ? _advice.reconnect : 'retry'; - switch (action) - { - case 'retry': - _increaseBackoff(); - _debug('Connect failure, backing off and retrying in {} ms', _backoff); - _delayedConnect(); - break; - case 'handshake': - _resetBackoff(); - _delayedHandshake(); - break; - case 'none': - _resetBackoff(); - break; - default: - _debug('Unrecognized reconnect value: {}', action); - break; - } - } - }; - - function _disconnectSuccess(message) - { - if (message.successful) - { - _debug('Disconnect successful'); - _disconnect(false); - _notifyListeners('/meta/disconnect', message); - } - else - { - _debug('Disconnect unsuccessful'); - _disconnect(true); - _notifyListeners('/meta/disconnect', message); - _notifyListeners('/meta/usuccessful', message); - } - }; - - function _disconnect(abort) - { - _cancelDelayedSend(); - if (abort) _transport.abort(); - _clientId = null; - _setStatus('disconnected'); - _batch = 0; - _messageQueue = []; - _resetBackoff(); - }; - - function _disconnectFailure(xhr, message) - { - _debug('Disconnect failure'); - _disconnect(true); - - var failureMessage = { - successful: false, - failure: true, - channel: '/meta/disconnect', - request: message, - xhr: xhr, - advice: { - action: 'none', - interval: 0 - } - }; - _notifyListeners('/meta/disconnect', failureMessage); - _notifyListeners('/meta/unsuccessful', failureMessage); - }; - - function _subscribeSuccess(message) - { - if (message.successful) - { - _debug('Subscribe successful'); - _notifyListeners('/meta/subscribe', message); - } - else - { - _debug('Subscribe unsuccessful'); - _notifyListeners('/meta/subscribe', message); - _notifyListeners('/meta/unsuccessful', message); - } - }; - - function _subscribeFailure(xhr, message) - { - _debug('Subscribe failure'); - - var failureMessage = { - successful: false, - failure: true, - channel: '/meta/subscribe', - request: message, - xhr: xhr, - advice: { - action: 'none', - interval: 0 - } - }; - _notifyListeners('/meta/subscribe', failureMessage); - _notifyListeners('/meta/unsuccessful', failureMessage); - }; - - function _unsubscribeSuccess(message) - { - if (message.successful) - { - _debug('Unsubscribe successful'); - _notifyListeners('/meta/unsubscribe', message); - } - else - { - _debug('Unsubscribe unsuccessful'); - _notifyListeners('/meta/unsubscribe', message); - _notifyListeners('/meta/unsuccessful', message); - } - }; - - function _unsubscribeFailure(xhr, message) - { - _debug('Unsubscribe failure'); - - var failureMessage = { - successful: false, - failure: true, - channel: '/meta/unsubscribe', - request: message, - xhr: xhr, - advice: { - action: 'none', - interval: 0 - } - }; - _notifyListeners('/meta/unsubscribe', failureMessage); - _notifyListeners('/meta/unsuccessful', failureMessage); - }; - - function _messageSuccess(message) - { - if (message.successful === undefined) - { - if (message.data) - { - // It is a plain message, and not a bayeux meta message - _notifyListeners(message.channel, message); - } - else - { - _debug('Unknown message {}', JSON.stringify(message)); - } - } - else - { - if (message.successful) - { - _debug('Publish successful'); - _notifyListeners('/meta/publish', message); - } - else - { - _debug('Publish unsuccessful'); - _notifyListeners('/meta/publish', message); - _notifyListeners('/meta/unsuccessful', message); - } - } - }; - - function _messageFailure(xhr, message) - { - _debug('Publish failure'); - - var failureMessage = { - successful: false, - failure: true, - channel: message.channel, - request: message, - xhr: xhr, - advice: { - action: 'none', - interval: 0 - } - }; - _notifyListeners('/meta/publish', failureMessage); - _notifyListeners('/meta/unsuccessful', failureMessage); - }; - - function _notifyListeners(channel, message) - { - // Notify direct listeners - _notify(channel, message); - - // Notify the globbing listeners - var channelParts = channel.split("/"); - var last = channelParts.length - 1; - for (var i = last; i > 0; --i) - { - var channelPart = channelParts.slice(0, i).join('/') + '/*'; - // We don't want to notify /foo/* if the channel is /foo/bar/baz, - // so we stop at the first non recursive globbing - if (i == last) _notify(channelPart, message); - // Add the recursive globber and notify - channelPart += '*'; - _notify(channelPart, message); - } - }; - - function _notify(channel, message) - { - var subscriptions = _listeners[channel]; - if (subscriptions && subscriptions.length > 0) - { - for (var i = 0; i < subscriptions.length; ++i) - { - var subscription = subscriptions[i]; - // Subscriptions may come and go, so the array may have 'holes' - if (subscription) - { - try - { - _debug('Notifying subscription: channel \'{}\', callback \'{}\'', channel, subscription.callback.name); - subscription.callback.call(subscription.scope, message); - } - catch (x) - { - // Ignore exceptions from callbacks - _warn('Exception during execution of callback \'{}\' on channel \'{}\' for message {}, exception: {}', subscription.callback.name, channel, JSON.stringify(message), x); - } - } - } - } - }; - - function _resetBackoff() - { - _backoff = 0; - }; - - function _increaseBackoff() - { - if (_backoff < _maxBackoff) _backoff += _backoffIncrement; - }; - - var _error = this._error = function(text, args) - { - _log('error', _format.apply(this, arguments)); - }; - - var _warn = this._warn = function(text, args) - { - _log('warn', _format.apply(this, arguments)); - }; - - var _info = this._info = function(text, args) - { - _log('info', _format.apply(this, arguments)); - }; - - var _debug = this._debug = function(text, args) - { - _log('debug', _format.apply(this, arguments)); - }; - - function _log(level, text) - { - var priority = _logPriorities[level]; - var configPriority = _logPriorities[_logLevel]; - if (!configPriority) configPriority = _logPriorities['info']; - if (priority >= configPriority) - { - if (window.console) window.console.log(text); - } - }; - - function _format(text) - { - var braces = /\{\}/g; - var result = ''; - var start = 0; - var count = 0; - while (braces.test(text)) - { - result += text.substr(start, braces.lastIndex - start - 2); - var arg = arguments[++count]; - result += arg !== undefined ? arg : '{}'; - start = braces.lastIndex; - } - result += text.substr(start, text.length - start); - return result; - }; - - function newLongPollingTransport() - { - return $.extend({}, new Transport('long-polling'), new LongPollingTransport()); - }; - - function newCallbackPollingTransport() - { - return $.extend({}, new Transport('callback-polling'), new CallbackPollingTransport()); - }; - - /** - * Base object with the common functionality for transports. - * The key responsibility is to allow at most 2 outstanding requests to the server, - * to avoid that requests are sent behind a long poll. - * To achieve this, we have one reserved request for the long poll, and all other - * requests are serialized one after the other. - */ - var Transport = function(type) - { - var _maxRequests = 2; - var _requestIds = 0; - var _cometRequest = null; - var _requests = []; - var _packets = []; - - this.getType = function() - { - return type; - }; - - this.send = function(packet, comet) - { - if (comet) - _cometSend(this, packet); - else - _send(this, packet); - }; - - function _cometSend(self, packet) - { - if (_cometRequest !== null) throw 'Concurrent comet requests not allowed, request ' + _cometRequest.id + ' not yet completed'; - - var requestId = ++_requestIds; - _debug('Beginning comet request {}', requestId); - - var request = {id: requestId}; - _debug('Delivering comet request {}', requestId); - self.deliver(packet, request); - _cometRequest = request; - }; - - function _send(self, packet) - { - var requestId = ++_requestIds; - _debug('Beginning request {}, {} other requests, {} queued requests', requestId, _requests.length, _packets.length); - - var request = {id: requestId}; - // Consider the comet request which should always be present - if (_requests.length < _maxRequests - 1) - { - _debug('Delivering request {}', requestId); - self.deliver(packet, request); - _requests.push(request); - } - else - { - _packets.push([packet, request]); - _debug('Queued request {}, {} queued requests', requestId, _packets.length); - } - }; - - this.complete = function(request, success, comet) - { - if (comet) - _cometComplete(request); - else - _complete(this, request, success); - }; - - function _cometComplete(request) - { - var requestId = request.id; - if (_cometRequest !== request) throw 'Comet request mismatch, completing request ' + requestId; - - // Reset comet request - _cometRequest = null; - _debug('Ended comet request {}', requestId); - }; - - function _complete(self, request, success) - { - var requestId = request.id; - var index = $.inArray(request, _requests); - // The index can be negative the request has been aborted - if (index >= 0) _requests.splice(index, 1); - _debug('Ended request {}, {} other requests, {} queued requests', requestId, _requests.length, _packets.length); - - if (_packets.length > 0) - { - var packet = _packets.shift(); - if (success) - { - _debug('Dequeueing and sending request {}, {} queued requests', packet[1].id, _packets.length); - _send(self, packet[0]); - } - else - { - _debug('Dequeueing and failing request {}, {} queued requests', packet[1].id, _packets.length); - // Keep the semantic of calling response callbacks asynchronously after the request - setTimeout(function() { packet[0].onFailure(packet[1], 'error'); }, 0); - } - } - }; - - this.abort = function() - { - for (var i = 0; i < _requests.length; ++i) - { - var request = _requests[i]; - _debug('Aborting request {}', request.id); - if (request.xhr) request.xhr.abort(); - } - if (_cometRequest) - { - _debug('Aborting comet request {}', _cometRequest.id); - if (_cometRequest.xhr) _cometRequest.xhr.abort(); - } - _cometRequest = null; - _requests = []; - _packets = []; - }; - }; - - var LongPollingTransport = function() - { - this.deliver = function(packet, request) - { - request.xhr = $.ajax({ - url: packet.url, - type: 'POST', - contentType: 'text/json;charset=UTF-8', - beforeSend: function(xhr) - { - xhr.setRequestHeader('Connection', 'Keep-Alive'); - return true; - }, - data: JSON.stringify(packet.messages), - success: function(response) { packet.onSuccess(request, response); }, - error: function(xhr, reason, exception) { packet.onFailure(request, reason, exception); } - }); - }; - }; - - var CallbackPollingTransport = function() - { - var _maxLength = 2000; - this.deliver = function(packet, request) - { - // Microsoft Internet Explorer has a 2083 URL max length - // We must ensure that we stay within that length - var messages = JSON.stringify(packet.messages); - // Encode the messages because all brackets, quotes, commas, colons, etc - // present in the JSON will be URL encoded, taking many more characters - var urlLength = packet.url.length + encodeURI(messages).length; - _debug('URL length: {}', urlLength); - // Let's stay on the safe side and use 2000 instead of 2083 - // also because we did not count few characters among which - // the parameter name 'message' and the parameter 'jsonp', - // which sum up to about 50 chars - if (urlLength > _maxLength) - { - var x = packet.messages.length > 1 ? - 'Too many bayeux messages in the same batch resulting in message too big ' + - '(' + urlLength + ' bytes, max is ' + _maxLength + ') for transport ' + this.getType() : - 'Bayeux message too big (' + urlLength + ' bytes, max is ' + _maxLength + ') ' + - 'for transport ' + this.getType(); - // Keep the semantic of calling response callbacks asynchronously after the request - _setTimeout(function() { packet.onFailure(request, 'error', x); }, 0); - } - else - { - $.ajax({ - url: packet.url, - type: 'GET', - dataType: 'jsonp', - jsonp: 'jsonp', - beforeSend: function(xhr) - { - xhr.setRequestHeader('Connection', 'Keep-Alive'); - return true; - }, - data: - { - // In callback-polling, the content must be sent via the 'message' parameter - message: messages - }, - success: function(response) { packet.onSuccess(request, response); }, - error: function(xhr, reason, exception) { packet.onFailure(request, reason, exception); } - }); - } - }; - }; - }; - - /** - * The JS object that exposes the comet API to applications - */ - $.cometd = new $.Cometd(); // The default instance - -})(jQuery); diff --git a/plugins/Comet/js/cometupdate.js b/plugins/Comet/js/cometupdate.js new file mode 100644 index 0000000000..50b02b7f34 --- /dev/null +++ b/plugins/Comet/js/cometupdate.js @@ -0,0 +1,27 @@ +// update the local timeline from a Comet server +var CometUpdate = function() +{ + var _server; + var _timeline; + var _userid; + var _replyurl; + var _favorurl; + var _deleteurl; + var _cometd; + + return { + init: function(server, timeline, userid, replyurl, favorurl, deleteurl) + { + _cometd = $.cometd; // Uses the default Comet object + _cometd.init(server); + _server = server; + _timeline = timeline; + _userid = userid; + _favorurl = favorurl; + _replyurl = replyurl; + _deleteurl = deleteurl; + _cometd.subscribe(timeline, function(message) { RealtimeUpdate.receive(message.data) }); + $(window).unload(function() { _cometd.disconnect(); } ); + } + } +}(); diff --git a/plugins/Comet/js/jquery.comet.js b/plugins/Comet/js/jquery.comet.js new file mode 100644 index 0000000000..6de437fa8e --- /dev/null +++ b/plugins/Comet/js/jquery.comet.js @@ -0,0 +1,1451 @@ +/** + * Copyright 2008 Mort Bay Consulting Pty. Ltd. + * Dual licensed under the Apache License 2.0 and the MIT license. + * ---------------------------------------------------------------------------- + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http: *www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ---------------------------------------------------------------------------- + * Licensed under the MIT license; + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * ---------------------------------------------------------------------------- + * $Revision$ $Date$ + */ +(function($) +{ + /** + * The constructor for a Comet object. + * There is a default Comet instance already created at the variable $.cometd, + * and hence that can be used to start a comet conversation with a server. + * In the rare case a page needs more than one comet conversation, a new instance can be + * created via: + *
+     * var url2 = ...;
+     * var cometd2 = new $.Cometd();
+     * cometd2.init(url2);
+     * 
+ */ + $.Cometd = function(name) + { + var _name = name || 'default'; + var _logPriorities = { debug: 1, info: 2, warn: 3, error: 4 }; + var _logLevel = 'info'; + var _url; + var _xd = false; + var _transport; + var _status = 'disconnected'; + var _messageId = 0; + var _clientId = null; + var _batch = 0; + var _messageQueue = []; + var _listeners = {}; + var _backoff = 0; + var _backoffIncrement = 1000; + var _maxBackoff = 60000; + var _scheduledSend = null; + var _extensions = []; + var _advice = {}; + var _handshakeProps; + + /** + * Returns the name assigned to this Comet object, or the string 'default' + * if no name has been explicitely passed as parameter to the constructor. + */ + this.getName = function() + { + return _name; + }; + + /** + * Configures the initial comet communication with the comet server. + * @param cometURL the URL of the comet server + */ + this.configure = function(cometURL) + { + _configure(cometURL); + }; + + function _configure(cometURL) + { + _url = cometURL; + _debug('Initializing comet with url: {}', _url); + + // Check immediately if we're cross domain + // If cross domain, the handshake must not send the long polling transport type + var urlParts = /(^https?:)?(\/\/(([^:\/\?#]+)(:(\d+))?))?([^\?#]*)/.exec(cometURL); + if (urlParts[3]) _xd = urlParts[3] != location.host; + + // Temporary setup a transport to send the initial handshake + // The transport may be changed as a result of handshake + if (_xd) + _transport = newCallbackPollingTransport(); + else + _transport = newLongPollingTransport(); + _debug('Initial transport is {}', _transport.getType()); + }; + + /** + * Configures and establishes the comet communication with the comet server + * via a handshake and a subsequent connect. + * @param cometURL the URL of the comet server + * @param handshakeProps an object to be merged with the handshake message + * @see #configure(cometURL) + * @see #handshake(handshakeProps) + */ + this.init = function(cometURL, handshakeProps) + { + _configure(cometURL); + _handshake(handshakeProps); + }; + + /** + * Establishes the comet communication with the comet server + * via a handshake and a subsequent connect. + * @param handshakeProps an object to be merged with the handshake message + */ + this.handshake = function(handshakeProps) + { + _handshake(handshakeProps); + }; + + /** + * Disconnects from the comet server. + * @param disconnectProps an object to be merged with the disconnect message + */ + this.disconnect = function(disconnectProps) + { + var bayeuxMessage = { + channel: '/meta/disconnect' + }; + var message = $.extend({}, disconnectProps, bayeuxMessage); + // Deliver immediately + // The handshake and connect mechanism make use of startBatch(), and in case + // of a failed handshake the disconnect would not be delivered if using _send(). + _setStatus('disconnecting'); + _deliver([message], false); + }; + + /** + * Marks the start of a batch of application messages to be sent to the server + * in a single request, obtaining a single response containing (possibly) many + * application reply messages. + * Messages are held in a queue and not sent until {@link #endBatch()} is called. + * If startBatch() is called multiple times, then an equal number of endBatch() + * calls must be made to close and send the batch of messages. + * @see #endBatch() + */ + this.startBatch = function() + { + _startBatch(); + }; + + /** + * Marks the end of a batch of application messages to be sent to the server + * in a single request. + * @see #startBatch() + */ + this.endBatch = function() + { + _endBatch(true); + }; + + /** + * Subscribes to the given channel, performing the given callback in the given scope + * when a message for the channel arrives. + * @param channel the channel to subscribe to + * @param scope the scope of the callback + * @param callback the callback to call when a message is delivered to the channel + * @param subscribeProps an object to be merged with the subscribe message + * @return the subscription handle to be passed to {@link #unsubscribe(object)} + */ + this.subscribe = function(channel, scope, callback, subscribeProps) + { + var subscription = this.addListener(channel, scope, callback); + + // Send the subscription message after the subscription registration to avoid + // races where the server would deliver a message to the subscribers, but here + // on the client the subscription has not been added yet to the data structures + var bayeuxMessage = { + channel: '/meta/subscribe', + subscription: channel + }; + var message = $.extend({}, subscribeProps, bayeuxMessage); + _send(message); + + return subscription; + }; + + /** + * Unsubscribes the subscription obtained with a call to {@link #subscribe(string, object, function)}. + * @param subscription the subscription to unsubscribe. + */ + this.unsubscribe = function(subscription, unsubscribeProps) + { + // Remove the local listener before sending the message + // This ensures that if the server fails, this client does not get notifications + this.removeListener(subscription); + var bayeuxMessage = { + channel: '/meta/unsubscribe', + subscription: subscription[0] + }; + var message = $.extend({}, unsubscribeProps, bayeuxMessage); + _send(message); + }; + + /** + * Publishes a message on the given channel, containing the given content. + * @param channel the channel to publish the message to + * @param content the content of the message + * @param publishProps an object to be merged with the publish message + */ + this.publish = function(channel, content, publishProps) + { + var bayeuxMessage = { + channel: channel, + data: content + }; + var message = $.extend({}, publishProps, bayeuxMessage); + _send(message); + }; + + /** + * Adds a listener for bayeux messages, performing the given callback in the given scope + * when a message for the given channel arrives. + * @param channel the channel the listener is interested to + * @param scope the scope of the callback + * @param callback the callback to call when a message is delivered to the channel + * @returns the subscription handle to be passed to {@link #removeListener(object)} + * @see #removeListener(object) + */ + this.addListener = function(channel, scope, callback) + { + // The data structure is a map, where each subscription + // holds the callback to be called and its scope. + + // Normalize arguments + if (!callback) + { + callback = scope; + scope = undefined; + } + + var subscription = { + scope: scope, + callback: callback + }; + + var subscriptions = _listeners[channel]; + if (!subscriptions) + { + subscriptions = []; + _listeners[channel] = subscriptions; + } + // Pushing onto an array appends at the end and returns the id associated with the element increased by 1. + // Note that if: + // a.push('a'); var hb=a.push('b'); delete a[hb-1]; var hc=a.push('c'); + // then: + // hc==3, a.join()=='a',,'c', a.length==3 + var subscriptionIndex = subscriptions.push(subscription) - 1; + _debug('Added listener: channel \'{}\', callback \'{}\', index {}', channel, callback.name, subscriptionIndex); + + // The subscription to allow removal of the listener is made of the channel and the index + return [channel, subscriptionIndex]; + }; + + /** + * Removes the subscription obtained with a call to {@link #addListener(string, object, function)}. + * @param subscription the subscription to unsubscribe. + */ + this.removeListener = function(subscription) + { + var subscriptions = _listeners[subscription[0]]; + if (subscriptions) + { + delete subscriptions[subscription[1]]; + _debug('Removed listener: channel \'{}\', index {}', subscription[0], subscription[1]); + } + }; + + /** + * Removes all listeners registered with {@link #addListener(channel, scope, callback)} or + * {@link #subscribe(channel, scope, callback)}. + */ + this.clearListeners = function() + { + _listeners = {}; + }; + + /** + * Returns a string representing the status of the bayeux communication with the comet server. + */ + this.getStatus = function() + { + return _status; + }; + + /** + * Sets the backoff period used to increase the backoff time when retrying an unsuccessful or failed message. + * Default value is 1 second, which means if there is a persistent failure the retries will happen + * after 1 second, then after 2 seconds, then after 3 seconds, etc. So for example with 15 seconds of + * elapsed time, there will be 5 retries (at 1, 3, 6, 10 and 15 seconds elapsed). + * @param period the backoff period to set + * @see #getBackoffIncrement() + */ + this.setBackoffIncrement = function(period) + { + _backoffIncrement = period; + }; + + /** + * Returns the backoff period used to increase the backoff time when retrying an unsuccessful or failed message. + * @see #setBackoffIncrement(period) + */ + this.getBackoffIncrement = function() + { + return _backoffIncrement; + }; + + /** + * Returns the backoff period to wait before retrying an unsuccessful or failed message. + */ + this.getBackoffPeriod = function() + { + return _backoff; + }; + + /** + * Sets the log level for console logging. + * Valid values are the strings 'error', 'warn', 'info' and 'debug', from + * less verbose to more verbose. + * @param level the log level string + */ + this.setLogLevel = function(level) + { + _logLevel = level; + }; + + /** + * Registers an extension whose callbacks are called for every incoming message + * (that comes from the server to this client implementation) and for every + * outgoing message (that originates from this client implementation for the + * server). + * The format of the extension object is the following: + *
+         * {
+         *     incoming: function(message) { ... },
+         *     outgoing: function(message) { ... }
+         * }
+         * Both properties are optional, but if they are present they will be called
+         * respectively for each incoming message and for each outgoing message.
+         * 
+ * @param name the name of the extension + * @param extension the extension to register + * @return true if the extension was registered, false otherwise + * @see #unregisterExtension(name) + */ + this.registerExtension = function(name, extension) + { + var existing = false; + for (var i = 0; i < _extensions.length; ++i) + { + var existingExtension = _extensions[i]; + if (existingExtension.name == name) + { + existing = true; + return false; + } + } + if (!existing) + { + _extensions.push({ + name: name, + extension: extension + }); + _debug('Registered extension \'{}\'', name); + return true; + } + else + { + _info('Could not register extension with name \'{}\': another extension with the same name already exists'); + return false; + } + }; + + /** + * Unregister an extension previously registered with + * {@link #registerExtension(name, extension)}. + * @param name the name of the extension to unregister. + * @return true if the extension was unregistered, false otherwise + */ + this.unregisterExtension = function(name) + { + var unregistered = false; + $.each(_extensions, function(index, extension) + { + if (extension.name == name) + { + _extensions.splice(index, 1); + unregistered = true; + _debug('Unregistered extension \'{}\'', name); + return false; + } + }); + return unregistered; + }; + + /** + * Starts a the batch of messages to be sent in a single request. + * @see _endBatch(deliverMessages) + */ + function _startBatch() + { + ++_batch; + }; + + /** + * Ends the batch of messages to be sent in a single request, + * optionally delivering messages present in the message queue depending + * on the given argument. + * @param deliverMessages whether to deliver the messages in the queue or not + * @see _startBatch() + */ + function _endBatch(deliverMessages) + { + --_batch; + if (_batch < 0) _batch = 0; + if (deliverMessages && _batch == 0 && !_isDisconnected()) + { + var messages = _messageQueue; + _messageQueue = []; + if (messages.length > 0) _deliver(messages, false); + } + }; + + function _nextMessageId() + { + return ++_messageId; + }; + + /** + * Converts the given response into an array of bayeux messages + * @param response the response to convert + * @return an array of bayeux messages obtained by converting the response + */ + function _convertToMessages(response) + { + if (response === undefined) return []; + if (response instanceof Array) return response; + if (response instanceof String || typeof response == 'string') return eval('(' + response + ')'); + if (response instanceof Object) return [response]; + throw 'Conversion Error ' + response + ', typeof ' + (typeof response); + }; + + function _setStatus(newStatus) + { + _debug('{} -> {}', _status, newStatus); + _status = newStatus; + }; + + function _isDisconnected() + { + return _status == 'disconnecting' || _status == 'disconnected'; + }; + + /** + * Sends the initial handshake message + */ + function _handshake(handshakeProps) + { + _debug('Starting handshake'); + _clientId = null; + + // Start a batch. + // This is needed because handshake and connect are async. + // It may happen that the application calls init() then subscribe() + // and the subscribe message is sent before the connect message, if + // the subscribe message is not held until the connect message is sent. + // So here we start a batch to hold temporarly any message until + // the connection is fully established. + _batch = 0; + _startBatch(); + + // Save the original properties provided by the user + // Deep copy to avoid the user to be able to change them later + _handshakeProps = $.extend(true, {}, handshakeProps); + + var bayeuxMessage = { + version: '1.0', + minimumVersion: '0.9', + channel: '/meta/handshake', + supportedConnectionTypes: _xd ? ['callback-polling'] : ['long-polling', 'callback-polling'] + }; + // Do not allow the user to mess with the required properties, + // so merge first the user properties and *then* the bayeux message + var message = $.extend({}, handshakeProps, bayeuxMessage); + + // We started a batch to hold the application messages, + // so here we must bypass it and deliver immediately. + _setStatus('handshaking'); + _deliver([message], false); + }; + + function _findTransport(handshakeResponse) + { + var transportTypes = handshakeResponse.supportedConnectionTypes; + if (_xd) + { + // If we are cross domain, check if the server supports it, that's the only option + if ($.inArray('callback-polling', transportTypes) >= 0) return _transport; + } + else + { + // Check if we can keep long-polling + if ($.inArray('long-polling', transportTypes) >= 0) return _transport; + + // The server does not support long-polling + if ($.inArray('callback-polling', transportTypes) >= 0) return newCallbackPollingTransport(); + } + return null; + }; + + function _delayedHandshake() + { + _setStatus('handshaking'); + _delayedSend(function() + { + _handshake(_handshakeProps); + }); + }; + + function _delayedConnect() + { + _setStatus('connecting'); + _delayedSend(function() + { + _connect(); + }); + }; + + function _delayedSend(operation) + { + _cancelDelayedSend(); + var delay = _backoff; + _debug("Delayed send: backoff {}, interval {}", _backoff, _advice.interval); + if (_advice.interval && _advice.interval > 0) + delay += _advice.interval; + _scheduledSend = _setTimeout(operation, delay); + }; + + function _cancelDelayedSend() + { + if (_scheduledSend !== null) clearTimeout(_scheduledSend); + _scheduledSend = null; + }; + + function _setTimeout(funktion, delay) + { + return setTimeout(function() + { + try + { + funktion(); + } + catch (x) + { + _debug('Exception during scheduled execution of function \'{}\': {}', funktion.name, x); + } + }, delay); + }; + + /** + * Sends the connect message + */ + function _connect() + { + _debug('Starting connect'); + var message = { + channel: '/meta/connect', + connectionType: _transport.getType() + }; + _setStatus('connecting'); + _deliver([message], true); + _setStatus('connected'); + }; + + function _send(message) + { + if (_batch > 0) + _messageQueue.push(message); + else + _deliver([message], false); + }; + + /** + * Delivers the messages to the comet server + * @param messages the array of messages to send + */ + function _deliver(messages, comet) + { + // We must be sure that the messages have a clientId. + // This is not guaranteed since the handshake may take time to return + // (and hence the clientId is not known yet) and the application + // may create other messages. + $.each(messages, function(index, message) + { + message['id'] = _nextMessageId(); + if (_clientId) message['clientId'] = _clientId; + messages[index] = _applyOutgoingExtensions(message); + }); + + var self = this; + var envelope = { + url: _url, + messages: messages, + onSuccess: function(request, response) + { + try + { + _handleSuccess.call(self, request, response, comet); + } + catch (x) + { + _debug('Exception during execution of success callback: {}', x); + } + }, + onFailure: function(request, reason, exception) + { + try + { + _handleFailure.call(self, request, messages, reason, exception, comet); + } + catch (x) + { + _debug('Exception during execution of failure callback: {}', x); + } + } + }; + _debug('Sending request to {}, message(s): {}', envelope.url, JSON.stringify(envelope.messages)); + _transport.send(envelope, comet); + }; + + function _applyIncomingExtensions(message) + { + for (var i = 0; i < _extensions.length; ++i) + { + var extension = _extensions[i]; + var callback = extension.extension.incoming; + if (callback && typeof callback === 'function') + { + _debug('Calling incoming extension \'{}\', callback \'{}\'', extension.name, callback.name); + message = _applyExtension(extension.name, callback, message) || message; + } + } + return message; + }; + + function _applyOutgoingExtensions(message) + { + for (var i = 0; i < _extensions.length; ++i) + { + var extension = _extensions[i]; + var callback = extension.extension.outgoing; + if (callback && typeof callback === 'function') + { + _debug('Calling outgoing extension \'{}\', callback \'{}\'', extension.name, callback.name); + message = _applyExtension(extension.name, callback, message) || message; + } + } + return message; + }; + + function _applyExtension(name, callback, message) + { + try + { + return callback(message); + } + catch (x) + { + _debug('Exception during execution of extension \'{}\': {}', name, x); + return message; + } + }; + + function _handleSuccess(request, response, comet) + { + var messages = _convertToMessages(response); + _debug('Received response {}', JSON.stringify(messages)); + + // Signal the transport it can deliver other queued requests + _transport.complete(request, true, comet); + + for (var i = 0; i < messages.length; ++i) + { + var message = messages[i]; + message = _applyIncomingExtensions(message); + + if (message.advice) _advice = message.advice; + + var channel = message.channel; + switch (channel) + { + case '/meta/handshake': + _handshakeSuccess(message); + break; + case '/meta/connect': + _connectSuccess(message); + break; + case '/meta/disconnect': + _disconnectSuccess(message); + break; + case '/meta/subscribe': + _subscribeSuccess(message); + break; + case '/meta/unsubscribe': + _unsubscribeSuccess(message); + break; + default: + _messageSuccess(message); + break; + } + } + }; + + function _handleFailure(request, messages, reason, exception, comet) + { + var xhr = request.xhr; + _debug('Request failed, status: {}, reason: {}, exception: {}', xhr && xhr.status, reason, exception); + + // Signal the transport it can deliver other queued requests + _transport.complete(request, false, comet); + + for (var i = 0; i < messages.length; ++i) + { + var message = messages[i]; + var channel = message.channel; + switch (channel) + { + case '/meta/handshake': + _handshakeFailure(xhr, message); + break; + case '/meta/connect': + _connectFailure(xhr, message); + break; + case '/meta/disconnect': + _disconnectFailure(xhr, message); + break; + case '/meta/subscribe': + _subscribeFailure(xhr, message); + break; + case '/meta/unsubscribe': + _unsubscribeFailure(xhr, message); + break; + default: + _messageFailure(xhr, message); + break; + } + } + }; + + function _handshakeSuccess(message) + { + if (message.successful) + { + _debug('Handshake successful'); + // Save clientId, figure out transport, then follow the advice to connect + _clientId = message.clientId; + + var newTransport = _findTransport(message); + if (newTransport === null) + { + throw 'Could not agree on transport with server'; + } + else + { + if (_transport.getType() != newTransport.getType()) + { + _debug('Changing transport from {} to {}', _transport.getType(), newTransport.getType()); + _transport = newTransport; + } + } + + // Notify the listeners + // Here the new transport is in place, as well as the clientId, so + // the listener can perform a publish() if it wants, and the listeners + // are notified before the connect below. + _notifyListeners('/meta/handshake', message); + + var action = _advice.reconnect ? _advice.reconnect : 'retry'; + switch (action) + { + case 'retry': + _delayedConnect(); + break; + default: + break; + } + } + else + { + _debug('Handshake unsuccessful'); + + var retry = !_isDisconnected() && _advice.reconnect != 'none'; + if (!retry) _setStatus('disconnected'); + + _notifyListeners('/meta/handshake', message); + _notifyListeners('/meta/unsuccessful', message); + + // Only try again if we haven't been disconnected and + // the advice permits us to retry the handshake + if (retry) + { + _increaseBackoff(); + _debug('Handshake failure, backing off and retrying in {} ms', _backoff); + _delayedHandshake(); + } + } + }; + + function _handshakeFailure(xhr, message) + { + _debug('Handshake failure'); + + // Notify listeners + var failureMessage = { + successful: false, + failure: true, + channel: '/meta/handshake', + request: message, + xhr: xhr, + advice: { + action: 'retry', + interval: _backoff + } + }; + + var retry = !_isDisconnected() && _advice.reconnect != 'none'; + if (!retry) _setStatus('disconnected'); + + _notifyListeners('/meta/handshake', failureMessage); + _notifyListeners('/meta/unsuccessful', failureMessage); + + // Only try again if we haven't been disconnected and the + // advice permits us to try again + if (retry) + { + _increaseBackoff(); + _debug('Handshake failure, backing off and retrying in {} ms', _backoff); + _delayedHandshake(); + } + }; + + function _connectSuccess(message) + { + var action = _isDisconnected() ? 'none' : (_advice.reconnect ? _advice.reconnect : 'retry'); + if (!_isDisconnected()) _setStatus(action == 'retry' ? 'connecting' : 'disconnecting'); + + if (message.successful) + { + _debug('Connect successful'); + + // End the batch and allow held messages from the application + // to go to the server (see _handshake() where we start the batch). + // The batch is ended before notifying the listeners, so that + // listeners can batch other cometd operations + _endBatch(true); + + // Notify the listeners after the status change but before the next connect + _notifyListeners('/meta/connect', message); + + // Connect was successful. + // Normally, the advice will say "reconnect: 'retry', interval: 0" + // and the server will hold the request, so when a response returns + // we immediately call the server again (long polling) + switch (action) + { + case 'retry': + _resetBackoff(); + _delayedConnect(); + break; + default: + _resetBackoff(); + _setStatus('disconnected'); + break; + } + } + else + { + _debug('Connect unsuccessful'); + + // Notify the listeners after the status change but before the next action + _notifyListeners('/meta/connect', message); + _notifyListeners('/meta/unsuccessful', message); + + // Connect was not successful. + // This may happen when the server crashed, the current clientId + // will be invalid, and the server will ask to handshake again + switch (action) + { + case 'retry': + _increaseBackoff(); + _delayedConnect(); + break; + case 'handshake': + // End the batch but do not deliver the messages until we connect successfully + _endBatch(false); + _resetBackoff(); + _delayedHandshake(); + break; + case 'none': + _resetBackoff(); + _setStatus('disconnected'); + break; + } + } + }; + + function _connectFailure(xhr, message) + { + _debug('Connect failure'); + + // Notify listeners + var failureMessage = { + successful: false, + failure: true, + channel: '/meta/connect', + request: message, + xhr: xhr, + advice: { + action: 'retry', + interval: _backoff + } + }; + _notifyListeners('/meta/connect', failureMessage); + _notifyListeners('/meta/unsuccessful', failureMessage); + + if (!_isDisconnected()) + { + var action = _advice.reconnect ? _advice.reconnect : 'retry'; + switch (action) + { + case 'retry': + _increaseBackoff(); + _debug('Connect failure, backing off and retrying in {} ms', _backoff); + _delayedConnect(); + break; + case 'handshake': + _resetBackoff(); + _delayedHandshake(); + break; + case 'none': + _resetBackoff(); + break; + default: + _debug('Unrecognized reconnect value: {}', action); + break; + } + } + }; + + function _disconnectSuccess(message) + { + if (message.successful) + { + _debug('Disconnect successful'); + _disconnect(false); + _notifyListeners('/meta/disconnect', message); + } + else + { + _debug('Disconnect unsuccessful'); + _disconnect(true); + _notifyListeners('/meta/disconnect', message); + _notifyListeners('/meta/usuccessful', message); + } + }; + + function _disconnect(abort) + { + _cancelDelayedSend(); + if (abort) _transport.abort(); + _clientId = null; + _setStatus('disconnected'); + _batch = 0; + _messageQueue = []; + _resetBackoff(); + }; + + function _disconnectFailure(xhr, message) + { + _debug('Disconnect failure'); + _disconnect(true); + + var failureMessage = { + successful: false, + failure: true, + channel: '/meta/disconnect', + request: message, + xhr: xhr, + advice: { + action: 'none', + interval: 0 + } + }; + _notifyListeners('/meta/disconnect', failureMessage); + _notifyListeners('/meta/unsuccessful', failureMessage); + }; + + function _subscribeSuccess(message) + { + if (message.successful) + { + _debug('Subscribe successful'); + _notifyListeners('/meta/subscribe', message); + } + else + { + _debug('Subscribe unsuccessful'); + _notifyListeners('/meta/subscribe', message); + _notifyListeners('/meta/unsuccessful', message); + } + }; + + function _subscribeFailure(xhr, message) + { + _debug('Subscribe failure'); + + var failureMessage = { + successful: false, + failure: true, + channel: '/meta/subscribe', + request: message, + xhr: xhr, + advice: { + action: 'none', + interval: 0 + } + }; + _notifyListeners('/meta/subscribe', failureMessage); + _notifyListeners('/meta/unsuccessful', failureMessage); + }; + + function _unsubscribeSuccess(message) + { + if (message.successful) + { + _debug('Unsubscribe successful'); + _notifyListeners('/meta/unsubscribe', message); + } + else + { + _debug('Unsubscribe unsuccessful'); + _notifyListeners('/meta/unsubscribe', message); + _notifyListeners('/meta/unsuccessful', message); + } + }; + + function _unsubscribeFailure(xhr, message) + { + _debug('Unsubscribe failure'); + + var failureMessage = { + successful: false, + failure: true, + channel: '/meta/unsubscribe', + request: message, + xhr: xhr, + advice: { + action: 'none', + interval: 0 + } + }; + _notifyListeners('/meta/unsubscribe', failureMessage); + _notifyListeners('/meta/unsuccessful', failureMessage); + }; + + function _messageSuccess(message) + { + if (message.successful === undefined) + { + if (message.data) + { + // It is a plain message, and not a bayeux meta message + _notifyListeners(message.channel, message); + } + else + { + _debug('Unknown message {}', JSON.stringify(message)); + } + } + else + { + if (message.successful) + { + _debug('Publish successful'); + _notifyListeners('/meta/publish', message); + } + else + { + _debug('Publish unsuccessful'); + _notifyListeners('/meta/publish', message); + _notifyListeners('/meta/unsuccessful', message); + } + } + }; + + function _messageFailure(xhr, message) + { + _debug('Publish failure'); + + var failureMessage = { + successful: false, + failure: true, + channel: message.channel, + request: message, + xhr: xhr, + advice: { + action: 'none', + interval: 0 + } + }; + _notifyListeners('/meta/publish', failureMessage); + _notifyListeners('/meta/unsuccessful', failureMessage); + }; + + function _notifyListeners(channel, message) + { + // Notify direct listeners + _notify(channel, message); + + // Notify the globbing listeners + var channelParts = channel.split("/"); + var last = channelParts.length - 1; + for (var i = last; i > 0; --i) + { + var channelPart = channelParts.slice(0, i).join('/') + '/*'; + // We don't want to notify /foo/* if the channel is /foo/bar/baz, + // so we stop at the first non recursive globbing + if (i == last) _notify(channelPart, message); + // Add the recursive globber and notify + channelPart += '*'; + _notify(channelPart, message); + } + }; + + function _notify(channel, message) + { + var subscriptions = _listeners[channel]; + if (subscriptions && subscriptions.length > 0) + { + for (var i = 0; i < subscriptions.length; ++i) + { + var subscription = subscriptions[i]; + // Subscriptions may come and go, so the array may have 'holes' + if (subscription) + { + try + { + _debug('Notifying subscription: channel \'{}\', callback \'{}\'', channel, subscription.callback.name); + subscription.callback.call(subscription.scope, message); + } + catch (x) + { + // Ignore exceptions from callbacks + _warn('Exception during execution of callback \'{}\' on channel \'{}\' for message {}, exception: {}', subscription.callback.name, channel, JSON.stringify(message), x); + } + } + } + } + }; + + function _resetBackoff() + { + _backoff = 0; + }; + + function _increaseBackoff() + { + if (_backoff < _maxBackoff) _backoff += _backoffIncrement; + }; + + var _error = this._error = function(text, args) + { + _log('error', _format.apply(this, arguments)); + }; + + var _warn = this._warn = function(text, args) + { + _log('warn', _format.apply(this, arguments)); + }; + + var _info = this._info = function(text, args) + { + _log('info', _format.apply(this, arguments)); + }; + + var _debug = this._debug = function(text, args) + { + _log('debug', _format.apply(this, arguments)); + }; + + function _log(level, text) + { + var priority = _logPriorities[level]; + var configPriority = _logPriorities[_logLevel]; + if (!configPriority) configPriority = _logPriorities['info']; + if (priority >= configPriority) + { + if (window.console) window.console.log(text); + } + }; + + function _format(text) + { + var braces = /\{\}/g; + var result = ''; + var start = 0; + var count = 0; + while (braces.test(text)) + { + result += text.substr(start, braces.lastIndex - start - 2); + var arg = arguments[++count]; + result += arg !== undefined ? arg : '{}'; + start = braces.lastIndex; + } + result += text.substr(start, text.length - start); + return result; + }; + + function newLongPollingTransport() + { + return $.extend({}, new Transport('long-polling'), new LongPollingTransport()); + }; + + function newCallbackPollingTransport() + { + return $.extend({}, new Transport('callback-polling'), new CallbackPollingTransport()); + }; + + /** + * Base object with the common functionality for transports. + * The key responsibility is to allow at most 2 outstanding requests to the server, + * to avoid that requests are sent behind a long poll. + * To achieve this, we have one reserved request for the long poll, and all other + * requests are serialized one after the other. + */ + var Transport = function(type) + { + var _maxRequests = 2; + var _requestIds = 0; + var _cometRequest = null; + var _requests = []; + var _packets = []; + + this.getType = function() + { + return type; + }; + + this.send = function(packet, comet) + { + if (comet) + _cometSend(this, packet); + else + _send(this, packet); + }; + + function _cometSend(self, packet) + { + if (_cometRequest !== null) throw 'Concurrent comet requests not allowed, request ' + _cometRequest.id + ' not yet completed'; + + var requestId = ++_requestIds; + _debug('Beginning comet request {}', requestId); + + var request = {id: requestId}; + _debug('Delivering comet request {}', requestId); + self.deliver(packet, request); + _cometRequest = request; + }; + + function _send(self, packet) + { + var requestId = ++_requestIds; + _debug('Beginning request {}, {} other requests, {} queued requests', requestId, _requests.length, _packets.length); + + var request = {id: requestId}; + // Consider the comet request which should always be present + if (_requests.length < _maxRequests - 1) + { + _debug('Delivering request {}', requestId); + self.deliver(packet, request); + _requests.push(request); + } + else + { + _packets.push([packet, request]); + _debug('Queued request {}, {} queued requests', requestId, _packets.length); + } + }; + + this.complete = function(request, success, comet) + { + if (comet) + _cometComplete(request); + else + _complete(this, request, success); + }; + + function _cometComplete(request) + { + var requestId = request.id; + if (_cometRequest !== request) throw 'Comet request mismatch, completing request ' + requestId; + + // Reset comet request + _cometRequest = null; + _debug('Ended comet request {}', requestId); + }; + + function _complete(self, request, success) + { + var requestId = request.id; + var index = $.inArray(request, _requests); + // The index can be negative the request has been aborted + if (index >= 0) _requests.splice(index, 1); + _debug('Ended request {}, {} other requests, {} queued requests', requestId, _requests.length, _packets.length); + + if (_packets.length > 0) + { + var packet = _packets.shift(); + if (success) + { + _debug('Dequeueing and sending request {}, {} queued requests', packet[1].id, _packets.length); + _send(self, packet[0]); + } + else + { + _debug('Dequeueing and failing request {}, {} queued requests', packet[1].id, _packets.length); + // Keep the semantic of calling response callbacks asynchronously after the request + setTimeout(function() { packet[0].onFailure(packet[1], 'error'); }, 0); + } + } + }; + + this.abort = function() + { + for (var i = 0; i < _requests.length; ++i) + { + var request = _requests[i]; + _debug('Aborting request {}', request.id); + if (request.xhr) request.xhr.abort(); + } + if (_cometRequest) + { + _debug('Aborting comet request {}', _cometRequest.id); + if (_cometRequest.xhr) _cometRequest.xhr.abort(); + } + _cometRequest = null; + _requests = []; + _packets = []; + }; + }; + + var LongPollingTransport = function() + { + this.deliver = function(packet, request) + { + request.xhr = $.ajax({ + url: packet.url, + type: 'POST', + contentType: 'text/json;charset=UTF-8', + beforeSend: function(xhr) + { + xhr.setRequestHeader('Connection', 'Keep-Alive'); + return true; + }, + data: JSON.stringify(packet.messages), + success: function(response) { packet.onSuccess(request, response); }, + error: function(xhr, reason, exception) { packet.onFailure(request, reason, exception); } + }); + }; + }; + + var CallbackPollingTransport = function() + { + var _maxLength = 2000; + this.deliver = function(packet, request) + { + // Microsoft Internet Explorer has a 2083 URL max length + // We must ensure that we stay within that length + var messages = JSON.stringify(packet.messages); + // Encode the messages because all brackets, quotes, commas, colons, etc + // present in the JSON will be URL encoded, taking many more characters + var urlLength = packet.url.length + encodeURI(messages).length; + _debug('URL length: {}', urlLength); + // Let's stay on the safe side and use 2000 instead of 2083 + // also because we did not count few characters among which + // the parameter name 'message' and the parameter 'jsonp', + // which sum up to about 50 chars + if (urlLength > _maxLength) + { + var x = packet.messages.length > 1 ? + 'Too many bayeux messages in the same batch resulting in message too big ' + + '(' + urlLength + ' bytes, max is ' + _maxLength + ') for transport ' + this.getType() : + 'Bayeux message too big (' + urlLength + ' bytes, max is ' + _maxLength + ') ' + + 'for transport ' + this.getType(); + // Keep the semantic of calling response callbacks asynchronously after the request + _setTimeout(function() { packet.onFailure(request, 'error', x); }, 0); + } + else + { + $.ajax({ + url: packet.url, + type: 'GET', + dataType: 'jsonp', + jsonp: 'jsonp', + beforeSend: function(xhr) + { + xhr.setRequestHeader('Connection', 'Keep-Alive'); + return true; + }, + data: + { + // In callback-polling, the content must be sent via the 'message' parameter + message: messages + }, + success: function(response) { packet.onSuccess(request, response); }, + error: function(xhr, reason, exception) { packet.onFailure(request, reason, exception); } + }); + } + }; + }; + }; + + /** + * The JS object that exposes the comet API to applications + */ + $.cometd = new $.Cometd(); // The default instance + +})(jQuery); diff --git a/plugins/LinkPreview/LinkPreviewPlugin.php b/plugins/LinkPreview/LinkPreviewPlugin.php index 301076ec94..0a1d6d0da8 100644 --- a/plugins/LinkPreview/LinkPreviewPlugin.php +++ b/plugins/LinkPreview/LinkPreviewPlugin.php @@ -52,12 +52,7 @@ class LinkPreviewPlugin extends Plugin { $user = common_current_user(); if ($user && common_config('attachments', 'process_links')) { - if (common_config('site', 'minify')) { - $js = 'linkpreview.min.js'; - } else { - $js = 'linkpreview.js'; - } - $action->script($this->path($js)); + $action->script($this->path('js/linkpreview.js')); $data = json_encode(array( 'api' => common_local_url('oembedproxy'), 'width' => common_config('attachments', 'thumbwidth'), diff --git a/plugins/LinkPreview/js/linkpreview.js b/plugins/LinkPreview/js/linkpreview.js new file mode 100644 index 0000000000..e6e98bdb4d --- /dev/null +++ b/plugins/LinkPreview/js/linkpreview.js @@ -0,0 +1,270 @@ +/** + * (c) 2010 StatusNet, Inc. + */ + +(function() { + /** + * Quickie wrapper around ooembed JSON lookup + */ + var oEmbed = { + api: 'https://noembed.com/embed', + width: 100, + height: 75, + cache: {}, + callbacks: {}, + + /** + * Do a cached oEmbed lookup for the given URL. + * + * @param {String} url + * @param {function} callback + */ + lookup: function(url, callback) + { + if (typeof oEmbed.cache[url] == "object") { + // We already have a successful lookup. + callback(oEmbed.cache[url]); + } else if (typeof oEmbed.callbacks[url] == "undefined") { + // No lookup yet... Start it! + oEmbed.callbacks[url] = [callback]; + + oEmbed.rawLookup(url, function(data) { + oEmbed.cache[url] = data; + var callbacks = oEmbed.callbacks[url]; + oEmbed.callbacks[url] = undefined; + for (var i = 0; i < callbacks.length; i++) { + callbacks[i](data); + } + }); + } else { + // A lookup is in progress. + oEmbed.callbacks[url].push(callback); + } + }, + + /** + * Do an oEmbed lookup for the given URL. + * + * @fixme proxy through ourselves if possible? + * @fixme use the global thumbnail size settings + * + * @param {String} url + * @param {function} callback + */ + rawLookup: function(url, callback) + { + var params = { + url: url, + format: 'json', + maxwidth: oEmbed.width, + maxheight: oEmbed.height, + token: $('#token').val() + }; + $.ajax({ + url: oEmbed.api, + data: params, + dataType: 'json', + success: function(data, xhr) { + callback(data); + }, + error: function(xhr, textStatus, errorThrown) { + callback(null); + } + }); + } + }; + + SN.Init.LinkPreview = function(params) { + if (params.api) oEmbed.api = params.api; + if (params.width) oEmbed.width = params.width; + if (params.height) oEmbed.height = params.height; + } + + // Piggyback on the counter update... + var origCounter = SN.U.Counter; + SN.U.Counter = function(form) { + var preview = form.data('LinkPreview'); + if (preview) { + preview.previewLinks(form.find('.notice_data-text:first').val()); + } + return origCounter(form); + } + + // Customize notice form init... + var origSetup = SN.Init.NoticeFormSetup; + SN.Init.NoticeFormSetup = function(form) { + origSetup(form); + + form + .bind('reset', function() { + LinkPreview.clear(); + }); + + var LinkPreview = { + links: [], + state: [], + refresh: [], + + /** + * Find URL links from the source text that may be interesting. + * + * @param {String} text + * @return {Array} list of URLs + */ + findLinks: function (text) + { + // @fixme match this to core code + var re = /(?:^| )(https?:\/\/.+?\/.+?)(?= |$)/mg; + var links = []; + var matches; + while ((matches = re.exec(text)) !== null) { + links.push(matches[1]); + } + return links; + }, + + ensureArea: function() { + if (form.find('.link-preview').length < 1) { + form.append(''); + } + }, + + /** + * Start looking up info for a link preview... + * May start async data loads. + * + * @param {number} col: column number to insert preview into + */ + prepLinkPreview: function(col) + { + var id = 'link-preview-' + col; + var url = LinkPreview.links[col]; + LinkPreview.refresh[col] = false; + LinkPreview.markLoading(col); + + oEmbed.lookup(url, function(data) { + var thumb = null; + var width = 100; + if (data && typeof data.thumbnail_url == "string") { + thumb = data.thumbnail_url; + if (typeof data.thumbnail_width !== "undefined") { + if (data.thumbnail_width < width) { + width = data.thumbnail_width; + } + } + } else if (data && data.type == 'photo' && typeof data.url == "string") { + thumb = data.url; + if (typeof data.width !== "undefined") { + if (data.width < width) { + width = data.width; + } + } + } + + if (thumb) { + LinkPreview.ensureArea(); + var link = $(''); + link.find('a') + .attr('href', url) + .attr('target', '_blank') + .last() + .find('img') + .attr('src', thumb) + .attr('width', width) + .attr('title', data.title || data.url || url); + form.find('.' + id) + .empty() + .append(link); + } else { + // No thumbnail available or error retriving it. + LinkPreview.clearLink(col); + } + + if (LinkPreview.refresh[col]) { + // Darn user has typed more characters. + // Go fetch another link! + LinkPreview.prepLinkPreview(col); + } else { + LinkPreview.markDone(col); + } + }); + }, + + /** + * Update the live preview section with links found in the given text. + * May start async data loads. + * + * @param {String} text: free-form input text + */ + previewLinks: function(text) + { + var i; + var old = LinkPreview.links; + var links = LinkPreview.findLinks(text); + LinkPreview.links = links; + + // Check for existing common elements... + for (i = 0; i < old.length && i < links.length; i++) { + if (links[i] != old[i]) { + if (LinkPreview.state[i] == "loading") { + // Slate this column for a refresh when this one's done. + LinkPreview.refresh[i] = true; + } else { + // Change an existing entry! + LinkPreview.prepLinkPreview(i); + } + } + } + if (links.length > old.length) { + // Adding new entries, whee! + for (i = old.length; i < links.length; i++) { + LinkPreview.addPreviewArea(i); + LinkPreview.prepLinkPreview(i); + } + } else if (old.length > links.length) { + // Remove preview entries for links that have been removed. + for (i = links.length; i < old.length; i++) { + LinkPreview.clearLink(i); + } + } + if (links.length == 0) { + LinkPreview.clear(); + } + }, + + addPreviewArea: function(col) { + LinkPreview.ensureArea(); + var id = 'link-preview-' + col; + if (form.find('.' + id).length < 1) { + form.find('.link-preview').append(''); + } + }, + + clearLink: function(col) { + var id = 'link-preview-' + col; + form.find('.' + id).html(''); + }, + + markLoading: function(col) { + LinkPreview.state[col] = "loading"; + var id = 'link-preview-' + col; + form.find('.' + id).attr('style', 'opacity: 0.5'); + }, + + markDone: function(col) { + LinkPreview.state[col] = "done"; + var id = 'link-preview-' + col; + form.find('.' + id).removeAttr('style'); + }, + + /** + * Clear out any link preview data. + */ + clear: function() { + LinkPreview.links = []; + form.find('.link-preview').remove(); + } + }; + form.data('LinkPreview', LinkPreview); + } +})(); diff --git a/plugins/LinkPreview/linkpreview.js b/plugins/LinkPreview/linkpreview.js deleted file mode 100644 index e6e98bdb4d..0000000000 --- a/plugins/LinkPreview/linkpreview.js +++ /dev/null @@ -1,270 +0,0 @@ -/** - * (c) 2010 StatusNet, Inc. - */ - -(function() { - /** - * Quickie wrapper around ooembed JSON lookup - */ - var oEmbed = { - api: 'https://noembed.com/embed', - width: 100, - height: 75, - cache: {}, - callbacks: {}, - - /** - * Do a cached oEmbed lookup for the given URL. - * - * @param {String} url - * @param {function} callback - */ - lookup: function(url, callback) - { - if (typeof oEmbed.cache[url] == "object") { - // We already have a successful lookup. - callback(oEmbed.cache[url]); - } else if (typeof oEmbed.callbacks[url] == "undefined") { - // No lookup yet... Start it! - oEmbed.callbacks[url] = [callback]; - - oEmbed.rawLookup(url, function(data) { - oEmbed.cache[url] = data; - var callbacks = oEmbed.callbacks[url]; - oEmbed.callbacks[url] = undefined; - for (var i = 0; i < callbacks.length; i++) { - callbacks[i](data); - } - }); - } else { - // A lookup is in progress. - oEmbed.callbacks[url].push(callback); - } - }, - - /** - * Do an oEmbed lookup for the given URL. - * - * @fixme proxy through ourselves if possible? - * @fixme use the global thumbnail size settings - * - * @param {String} url - * @param {function} callback - */ - rawLookup: function(url, callback) - { - var params = { - url: url, - format: 'json', - maxwidth: oEmbed.width, - maxheight: oEmbed.height, - token: $('#token').val() - }; - $.ajax({ - url: oEmbed.api, - data: params, - dataType: 'json', - success: function(data, xhr) { - callback(data); - }, - error: function(xhr, textStatus, errorThrown) { - callback(null); - } - }); - } - }; - - SN.Init.LinkPreview = function(params) { - if (params.api) oEmbed.api = params.api; - if (params.width) oEmbed.width = params.width; - if (params.height) oEmbed.height = params.height; - } - - // Piggyback on the counter update... - var origCounter = SN.U.Counter; - SN.U.Counter = function(form) { - var preview = form.data('LinkPreview'); - if (preview) { - preview.previewLinks(form.find('.notice_data-text:first').val()); - } - return origCounter(form); - } - - // Customize notice form init... - var origSetup = SN.Init.NoticeFormSetup; - SN.Init.NoticeFormSetup = function(form) { - origSetup(form); - - form - .bind('reset', function() { - LinkPreview.clear(); - }); - - var LinkPreview = { - links: [], - state: [], - refresh: [], - - /** - * Find URL links from the source text that may be interesting. - * - * @param {String} text - * @return {Array} list of URLs - */ - findLinks: function (text) - { - // @fixme match this to core code - var re = /(?:^| )(https?:\/\/.+?\/.+?)(?= |$)/mg; - var links = []; - var matches; - while ((matches = re.exec(text)) !== null) { - links.push(matches[1]); - } - return links; - }, - - ensureArea: function() { - if (form.find('.link-preview').length < 1) { - form.append(''); - } - }, - - /** - * Start looking up info for a link preview... - * May start async data loads. - * - * @param {number} col: column number to insert preview into - */ - prepLinkPreview: function(col) - { - var id = 'link-preview-' + col; - var url = LinkPreview.links[col]; - LinkPreview.refresh[col] = false; - LinkPreview.markLoading(col); - - oEmbed.lookup(url, function(data) { - var thumb = null; - var width = 100; - if (data && typeof data.thumbnail_url == "string") { - thumb = data.thumbnail_url; - if (typeof data.thumbnail_width !== "undefined") { - if (data.thumbnail_width < width) { - width = data.thumbnail_width; - } - } - } else if (data && data.type == 'photo' && typeof data.url == "string") { - thumb = data.url; - if (typeof data.width !== "undefined") { - if (data.width < width) { - width = data.width; - } - } - } - - if (thumb) { - LinkPreview.ensureArea(); - var link = $(''); - link.find('a') - .attr('href', url) - .attr('target', '_blank') - .last() - .find('img') - .attr('src', thumb) - .attr('width', width) - .attr('title', data.title || data.url || url); - form.find('.' + id) - .empty() - .append(link); - } else { - // No thumbnail available or error retriving it. - LinkPreview.clearLink(col); - } - - if (LinkPreview.refresh[col]) { - // Darn user has typed more characters. - // Go fetch another link! - LinkPreview.prepLinkPreview(col); - } else { - LinkPreview.markDone(col); - } - }); - }, - - /** - * Update the live preview section with links found in the given text. - * May start async data loads. - * - * @param {String} text: free-form input text - */ - previewLinks: function(text) - { - var i; - var old = LinkPreview.links; - var links = LinkPreview.findLinks(text); - LinkPreview.links = links; - - // Check for existing common elements... - for (i = 0; i < old.length && i < links.length; i++) { - if (links[i] != old[i]) { - if (LinkPreview.state[i] == "loading") { - // Slate this column for a refresh when this one's done. - LinkPreview.refresh[i] = true; - } else { - // Change an existing entry! - LinkPreview.prepLinkPreview(i); - } - } - } - if (links.length > old.length) { - // Adding new entries, whee! - for (i = old.length; i < links.length; i++) { - LinkPreview.addPreviewArea(i); - LinkPreview.prepLinkPreview(i); - } - } else if (old.length > links.length) { - // Remove preview entries for links that have been removed. - for (i = links.length; i < old.length; i++) { - LinkPreview.clearLink(i); - } - } - if (links.length == 0) { - LinkPreview.clear(); - } - }, - - addPreviewArea: function(col) { - LinkPreview.ensureArea(); - var id = 'link-preview-' + col; - if (form.find('.' + id).length < 1) { - form.find('.link-preview').append(''); - } - }, - - clearLink: function(col) { - var id = 'link-preview-' + col; - form.find('.' + id).html(''); - }, - - markLoading: function(col) { - LinkPreview.state[col] = "loading"; - var id = 'link-preview-' + col; - form.find('.' + id).attr('style', 'opacity: 0.5'); - }, - - markDone: function(col) { - LinkPreview.state[col] = "done"; - var id = 'link-preview-' + col; - form.find('.' + id).removeAttr('style'); - }, - - /** - * Clear out any link preview data. - */ - clear: function() { - LinkPreview.links = []; - form.find('.link-preview').remove(); - } - }; - form.data('LinkPreview', LinkPreview); - } -})(); diff --git a/plugins/LinkPreview/linkpreview.min.js b/plugins/LinkPreview/linkpreview.min.js deleted file mode 100644 index 99af83913e..0000000000 --- a/plugins/LinkPreview/linkpreview.min.js +++ /dev/null @@ -1 +0,0 @@ -(function(){var b={api:"https://noembed.com/embed",width:100,height:75,cache:{},callbacks:{},lookup:function(d,e){if(typeof b.cache[d]=="object"){e(b.cache[d])}else{if(typeof b.callbacks[d]=="undefined"){b.callbacks[d]=[e];b.rawLookup(d,function(h){b.cache[d]=h;var g=b.callbacks[d];b.callbacks[d]=undefined;for(var f=0;f')}},prepLinkPreview:function(g){var h="link-preview-"+g;var f=e.links[g];e.refresh[g]=false;e.markLoading(g);b.lookup(f,function(l){var i=null;var j=100;if(l&&typeof l.thumbnail_url=="string"){i=l.thumbnail_url;if(typeof l.thumbnail_width!=="undefined"){if(l.thumbnail_width');k.find("a").attr("href",f).attr("target","_blank").last().find("img").attr("src",i).attr("width",j).attr("title",l.title||l.url||f);d.find("."+h).empty().append(k)}else{e.clearLink(g)}if(e.refresh[g]){e.prepLinkPreview(g)}else{e.markDone(g)}})},previewLinks:function(j){var h;var f=e.links;var g=e.findLinks(j);e.links=g;for(h=0;hf.length){for(h=f.length;hg.length){for(h=g.length;h')}},clearLink:function(f){var g="link-preview-"+f;d.find("."+g).html("")},markLoading:function(f){e.state[f]="loading";var g="link-preview-"+f;d.find("."+g).attr("style","opacity: 0.5")},markDone:function(f){e.state[f]="done";var g="link-preview-"+f;d.find("."+g).removeAttr("style")},clear:function(){e.links=[];d.find(".link-preview").remove()}};d.data("LinkPreview",e)}})(); \ No newline at end of file diff --git a/plugins/Meteor/MeteorPlugin.php b/plugins/Meteor/MeteorPlugin.php index d65fe3f0e5..b495fcddd9 100644 --- a/plugins/Meteor/MeteorPlugin.php +++ b/plugins/Meteor/MeteorPlugin.php @@ -27,12 +27,10 @@ * @link http://status.net/ */ -if (!defined('STATUSNET') && !defined('LACONICA')) { +if (!defined('GNUSOCIAL') && !defined('STATUSNET')) { exit(1); } -require_once INSTALLDIR.'/plugins/Realtime/RealtimePlugin.php'; - /** * Plugin to do realtime updates using Meteor * @@ -96,7 +94,7 @@ class MeteorPlugin extends RealtimePlugin } else { $scripts[] = 'http://'.$this->webserver.(($this->webport == 80) ? '':':'.$this->webport).'/meteor.js'; } - $scripts[] = $this->path('meteorupdater.min.js'); + $scripts[] = $this->path('js/meteorupdater.js'); return $scripts; } diff --git a/plugins/Meteor/js/meteorupdater.js b/plugins/Meteor/js/meteorupdater.js new file mode 100644 index 0000000000..cdd1d63fab --- /dev/null +++ b/plugins/Meteor/js/meteorupdater.js @@ -0,0 +1,20 @@ +// Update the local timeline from a Meteor server + +var MeteorUpdater = function() +{ + return { + + init: function(server, port, timeline) + { + Meteor.callbacks["process"] = function(data) { + RealtimeUpdate.receive(JSON.parse(data)); + }; + + Meteor.host = server; + Meteor.port = port; + Meteor.joinChannel(timeline, 0); + Meteor.connect(); + } + } +}(); + diff --git a/plugins/Meteor/meteorupdater.js b/plugins/Meteor/meteorupdater.js deleted file mode 100644 index cdd1d63fab..0000000000 --- a/plugins/Meteor/meteorupdater.js +++ /dev/null @@ -1,20 +0,0 @@ -// Update the local timeline from a Meteor server - -var MeteorUpdater = function() -{ - return { - - init: function(server, port, timeline) - { - Meteor.callbacks["process"] = function(data) { - RealtimeUpdate.receive(JSON.parse(data)); - }; - - Meteor.host = server; - Meteor.port = port; - Meteor.joinChannel(timeline, 0); - Meteor.connect(); - } - } -}(); - diff --git a/plugins/Meteor/meteorupdater.min.js b/plugins/Meteor/meteorupdater.min.js deleted file mode 100644 index 61928ab4f2..0000000000 --- a/plugins/Meteor/meteorupdater.min.js +++ /dev/null @@ -1 +0,0 @@ -var MeteorUpdater=function(){return{init:function(c,a,b){Meteor.callbacks.process=function(d){RealtimeUpdate.receive(JSON.parse(d))};Meteor.host=c;Meteor.port=a;Meteor.joinChannel(b,0);Meteor.connect()}}}(); \ No newline at end of file diff --git a/plugins/Orbited/OrbitedPlugin.php b/plugins/Orbited/OrbitedPlugin.php index 6ae394b61d..6267b38f7e 100644 --- a/plugins/Orbited/OrbitedPlugin.php +++ b/plugins/Orbited/OrbitedPlugin.php @@ -27,12 +27,10 @@ * @link http://laconi.ca/ */ -if (!defined('LACONICA')) { +if (!defined('GNUSOCIAL') && !defined('STATUSNET')) { exit(1); } -require_once INSTALLDIR.'/plugins/Realtime/RealtimePlugin.php'; - /** * Plugin to do realtime updates using Orbited + STOMP * @@ -76,9 +74,9 @@ class OrbitedPlugin extends RealtimePlugin $root = 'http://'.$server.(($port == 80) ? '':':'.$port); $scripts[] = $root.'/static/Orbited.js'; - $scripts[] = 'plugins/Orbited/orbitedextra.js'; + $scripts[] = $this->path('js/orbitedextra.js'); $scripts[] = $root.'/static/protocols/stomp/stomp.js'; - $scripts[] = 'plugins/Orbited/orbitedupdater.js'; + $scripts[] = $this->path('js/orbitedupdater.js'); return $scripts; } diff --git a/plugins/Orbited/js/orbitedextra.js b/plugins/Orbited/js/orbitedextra.js new file mode 100644 index 0000000000..47e5c0c80e --- /dev/null +++ b/plugins/Orbited/js/orbitedextra.js @@ -0,0 +1,2 @@ +TCPSocket = Orbited.TCPSocket; + diff --git a/plugins/Orbited/js/orbitedupdater.js b/plugins/Orbited/js/orbitedupdater.js new file mode 100644 index 0000000000..8c5ab3b732 --- /dev/null +++ b/plugins/Orbited/js/orbitedupdater.js @@ -0,0 +1,24 @@ +// Update the local timeline from a Orbited server + +var OrbitedUpdater = function() +{ + return { + + init: function(server, port, timeline, username, password) + { + // set up stomp client. + stomp = new STOMPClient(); + + stomp.onmessageframe = function(frame) { + RealtimeUpdate.receive(JSON.parse(frame.body)); + }; + + stomp.onconnectedframe = function() { + stomp.subscribe(timeline); + } + + stomp.connect(server, port, username, password); + } + } +}(); + diff --git a/plugins/Orbited/orbitedextra.js b/plugins/Orbited/orbitedextra.js deleted file mode 100644 index 47e5c0c80e..0000000000 --- a/plugins/Orbited/orbitedextra.js +++ /dev/null @@ -1,2 +0,0 @@ -TCPSocket = Orbited.TCPSocket; - diff --git a/plugins/Orbited/orbitedupdater.js b/plugins/Orbited/orbitedupdater.js deleted file mode 100644 index 8c5ab3b732..0000000000 --- a/plugins/Orbited/orbitedupdater.js +++ /dev/null @@ -1,24 +0,0 @@ -// Update the local timeline from a Orbited server - -var OrbitedUpdater = function() -{ - return { - - init: function(server, port, timeline, username, password) - { - // set up stomp client. - stomp = new STOMPClient(); - - stomp.onmessageframe = function(frame) { - RealtimeUpdate.receive(JSON.parse(frame.body)); - }; - - stomp.onconnectedframe = function() { - stomp.subscribe(timeline); - } - - stomp.connect(server, port, username, password); - } - } -}(); - diff --git a/plugins/Realtime/RealtimePlugin.php b/plugins/Realtime/RealtimePlugin.php index 958716fa76..138ed4dc7a 100644 --- a/plugins/Realtime/RealtimePlugin.php +++ b/plugins/Realtime/RealtimePlugin.php @@ -142,7 +142,7 @@ class RealtimePlugin extends Plugin public function onEndShowStylesheets(Action $action) { - $action->cssLink(Plugin::staticPath('Realtime', 'realtimeupdate.css'), + $action->cssLink($this->path('css/realtimeupdate.css'), null, 'screen, projection, tv'); return true; @@ -391,12 +391,7 @@ class RealtimePlugin extends Plugin function _getScripts() { - if (common_config('site', 'minify')) { - $js = 'realtimeupdate.min.js'; - } else { - $js = 'realtimeupdate.js'; - } - return array(Plugin::staticPath('Realtime', $js)); + return array($this->path('js/realtimeupdate.js')); } /** diff --git a/plugins/Realtime/css/realtimeupdate.css b/plugins/Realtime/css/realtimeupdate.css new file mode 100644 index 0000000000..3295fe4a31 --- /dev/null +++ b/plugins/Realtime/css/realtimeupdate.css @@ -0,0 +1,76 @@ +.realtime-popup address { +display:none; +} + +.realtime-popup #content { +width:93.5%; +} + +.realtime-popup #form_notice { +margin:18px 0 18px 1.795%; +width:93%; +max-width:451px; +} + +.realtime-popup #form_notice label[for=notice_data-text], +.realtime-popup h1 { +display:none; +} + +.realtime-popup #form_notice label.notice_data-attach, +.realtime-popup #form_notice input.notice_data-attach, +.realtime-popup #form_notice label.notice_data-geo { +top:0; +} + +.realtime-popup #form_notice input.notice_data-attach { +left:auto; +right:0; +} + +.realtime-popup .entity_profile { +width:70%; +} +.realtime-popup .entity_actions { +margin-left:1%; +} + +#notices_primary { +position:relative; +} + +#realtime_actions { +position: absolute; +top: -20px; +right: 0; +margin: 0 0 11px 0; +} + +#realtime_actions li { +margin-left: 18px; +list-style-type: none; +float: left; +} + +#realtime_actions button { +width: 16px; +height: 16px; +display: block; +border: none; +cursor: pointer; +text-indent: -9999px; +float: left; +} + +#realtime_play { +margin-left: 4px; +} + +#queued_counter { +float:left; +line-height:1.2; +} + +#showstream #notices_primary { +margin-top: 18px; +} diff --git a/plugins/Realtime/js/realtimeupdate.js b/plugins/Realtime/js/realtimeupdate.js new file mode 100644 index 0000000000..90d0a05b09 --- /dev/null +++ b/plugins/Realtime/js/realtimeupdate.js @@ -0,0 +1,644 @@ +/* + * StatusNet - a distributed open-source microblogging tool + * Copyright (C) 2009-2011, StatusNet, Inc. + * + * Add a notice encoded as JSON into the current timeline + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * @category Plugin + * @package StatusNet + * @author Evan Prodromou + * @author Sarven Capadisli + * @copyright 2009-2011 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +/** + * This is the UI portion of the Realtime plugin base class, handling + * queueing up and displaying of notices that have been received through + * other code in one of the subclassed plugin implementations such as + * Meteor or Orbited. + * + * Notices are passed in as JSON objects formatted per the Twitter-compatible + * API. + * + * @todo Currently we duplicate a lot of formatting and layout code from + * the PHP side of StatusNet, which makes it very difficult to maintain + * this package. Internationalization as well as newer features such + * as location data, customized source links for OStatus profiles, + * and image thumbnails are not yet supported in Realtime yet because + * they have not been implemented here. + */ +RealtimeUpdate = { + _userid: 0, + _showurl: '', + _keepaliveurl: '', + _closeurl: '', + _updatecounter: 0, + _maxnotices: 50, + _windowhasfocus: true, + _documenttitle: '', + _paused:false, + _queuedNotices:[], + + /** + * Initialize the Realtime plugin UI on a page with a timeline view. + * + * This function is called from a JS fragment inserted by the PHP side + * of the Realtime plugin, and provides us with base information + * needed to build a near-replica of StatusNet's NoticeListItem output. + * + * Once the UI is initialized, a plugin subclass will need to actually + * feed data into the RealtimeUpdate object! + * + * @param {int} userid: local profile ID of the currently logged-in user + * @param {String} showurl: URL for shownotice action, used when fetching formatting notices. + * This URL contains a stub value of 0000000000 which will be replaced with the notice ID. + * + * @access public + */ + init: function(userid, showurl) + { + RealtimeUpdate._userid = userid; + RealtimeUpdate._showurl = showurl; + + RealtimeUpdate._documenttitle = document.title; + + $(window).bind('focus', function() { + RealtimeUpdate._windowhasfocus = true; + + // Clear the counter on the window title when we focus in. + RealtimeUpdate._updatecounter = 0; + RealtimeUpdate.removeWindowCounter(); + }); + + $(window).bind('blur', function() { + $('#notices_primary .notice').removeClass('mark-top'); + + $('#notices_primary .notice:first').addClass('mark-top'); + + // While we're in the background, received messages will increment + // a counter that we put on the window title. This will cause some + // browsers to also flash or mark the tab or window title bar until + // you seek attention (eg Firefox 4 pinned app tabs). + RealtimeUpdate._windowhasfocus = false; + + return false; + }); + }, + + /** + * Accept a notice in a Twitter-API JSON style and either show it + * or queue it up, depending on whether the realtime display is + * active. + * + * The meat of a Realtime plugin subclass is to provide a substrate + * transport to receive data and shove it into this function. :) + * + * Note that the JSON data is extended from the standard API return + * with additional fields added by RealtimePlugin's PHP code. + * + * @param {Object} data: extended JSON API-formatted notice + * + * @access public + */ + receive: function(data) + { + if (RealtimeUpdate.isNoticeVisible(data.id)) { + // Probably posted by the user in this window, and so already + // shown by the AJAX form handler. Ignore it. + return; + } + if (RealtimeUpdate._paused === false) { + RealtimeUpdate.purgeLastNoticeItem(); + + RealtimeUpdate.insertNoticeItem(data); + } + else { + RealtimeUpdate._queuedNotices.push(data); + + RealtimeUpdate.updateQueuedCounter(); + } + + RealtimeUpdate.updateWindowCounter(); + }, + + /** + * Add a visible representation of the given notice at the top of + * the current timeline. + * + * If the notice is already in the timeline, nothing will be added. + * + * @param {Object} data: extended JSON API-formatted notice + * + * @fixme while core UI JS code is used to activate the AJAX UI controls, + * the actual production of HTML (in makeNoticeItem and its subs) + * duplicates core code without plugin hook points or i18n support. + * + * @access private + */ + insertNoticeItem: function(data) { + // Don't add it if it already exists + if (RealtimeUpdate.isNoticeVisible(data.id)) { + return; + } + + RealtimeUpdate.makeNoticeItem(data, function(noticeItem) { + // Check again in case it got shown while we were waiting for data... + if (RealtimeUpdate.isNoticeVisible(data.id)) { + return; + } + var noticeItemID = $(noticeItem).attr('id'); + + var list = $("#notices_primary .notices:first") + var prepend = true; + + var threaded = list.hasClass('threaded-notices'); + if (threaded && data.in_reply_to_status_id) { + // aho! + var parent = $('#notice-' + data.in_reply_to_status_id); + if (parent.length == 0) { + // @todo fetch the original, insert it, and finish the rest + } else { + // Check the parent notice to make sure it's not a reply itself. + // If so, use it's parent as the parent. + var parentList = parent.closest('.notices'); + if (parentList.hasClass('threaded-replies')) { + parent = parentList.closest('.notice'); + } + list = parent.find('.threaded-replies'); + if (list.length == 0) { + list = $('
    '); + parent.append(list); + SN.U.NoticeInlineReplyPlaceholder(parent); + } + prepend = false; + } + } + + var newNotice = $(noticeItem); + if (prepend) { + list.prepend(newNotice); + } else { + var placeholder = list.find('li.notice-reply-placeholder') + if (placeholder.length > 0) { + newNotice.insertBefore(placeholder) + } else { + newNotice.appendTo(list); + } + } + newNotice.css({display:"none"}).fadeIn(1000); + + SN.U.NoticeReplyTo($('#'+noticeItemID)); + SN.U.NoticeWithAttachment($('#'+noticeItemID)); + }); + }, + + /** + * Check if the given notice is visible in the timeline currently. + * Used to avoid duplicate processing of notices that have been + * displayed by other means. + * + * @param {number} id: notice ID to check + * + * @return boolean + * + * @access private + */ + isNoticeVisible: function(id) { + return ($("#notice-"+id).length > 0); + }, + + /** + * Trims a notice off the end of the timeline if we have more than the + * maximum number of notices visible. + * + * @access private + */ + purgeLastNoticeItem: function() { + if ($('#notices_primary .notice').length > RealtimeUpdate._maxnotices) { + $("#notices_primary .notice:last").remove(); + } + }, + + /** + * If the window/tab is in background, increment the counter of newly + * received notices and append it onto the window title. + * + * Has no effect if the window is in foreground. + * + * @access private + */ + updateWindowCounter: function() { + if (RealtimeUpdate._windowhasfocus === false) { + RealtimeUpdate._updatecounter += 1; + document.title = '('+RealtimeUpdate._updatecounter+') ' + RealtimeUpdate._documenttitle; + } + }, + + /** + * Clear the background update counter from the window title. + * + * @access private + * + * @fixme could interfere with anything else trying similar tricks + */ + removeWindowCounter: function() { + document.title = RealtimeUpdate._documenttitle; + }, + + /** + * Builds a notice HTML block from JSON API-style data; + * loads data from server, so runs async. + * + * @param {Object} data: extended JSON API-formatted notice + * @param {function} callback: function(DOMNode) to receive new code + * + * @access private + */ + makeNoticeItem: function(data, callback) + { + var url = RealtimeUpdate._showurl.replace('0000000000', data.id); + $.get(url, {ajax: 1}, function(data, textStatus, xhr) { + var notice = $('li.notice:first', data); + if (notice.length) { + var node = document._importNode(notice[0], true); + callback(node); + } + }); + }, + + /** + * Creates a favorite button. + * + * @param {number} id: notice ID to work with + * @param {String} session_key: session token for form CSRF protection + * @return {String} HTML fragment + * + * @fixme this replicates core StatusNet code, making maintenance harder + * @fixme sloppy HTML building (raw concat without escaping) + * @fixme no i18n support + * + * @access private + */ + makeFavoriteForm: function(id, session_key) + { + var ff; + + ff = "
    "+ + "
    "+ + "Favor this notice"+ + ""+ + ""+ + ""+ + "
    "+ + "
    "; + return ff; + }, + + /** + * Creates a reply button. + * + * @param {number} id: notice ID to work with + * @param {String} nickname: nick of the user to whom we are replying + * @return {String} HTML fragment + * + * @fixme this replicates core StatusNet code, making maintenance harder + * @fixme sloppy HTML building (raw concat without escaping) + * @fixme no i18n support + * + * @access private + */ + makeReplyLink: function(id, nickname) + { + var rl; + rl = "Reply "+id+""; + return rl; + }, + + /** + * Creates a repeat button. + * + * @param {number} id: notice ID to work with + * @param {String} session_key: session token for form CSRF protection + * @return {String} HTML fragment + * + * @fixme this replicates core StatusNet code, making maintenance harder + * @fixme sloppy HTML building (raw concat without escaping) + * @fixme no i18n support + * + * @access private + */ + makeRepeatForm: function(id, session_key) + { + var rf; + rf = "
    "+ + "
    "+ + "Repeat this notice?"+ + ""+ + ""+ + ""+ + "
    "+ + "
    "; + + return rf; + }, + + /** + * Creates a delete button. + * + * @param {number} id: notice ID to create a delete link for + * @return {String} HTML fragment + * + * @fixme this replicates core StatusNet code, making maintenance harder + * @fixme sloppy HTML building (raw concat without escaping) + * @fixme no i18n support + * + * @access private + */ + makeDeleteLink: function(id) + { + var dl, delurl; + delurl = RealtimeUpdate._deleteurl.replace("0000000000", id); + + dl = "Delete"; + + return dl; + }, + + /** + * Adds a control widget at the top of the timeline view, containing + * pause/play and popup buttons. + * + * @param {String} url: full URL to the popup window variant of this timeline page + * @param {String} timeline: string key for the timeline (eg 'public' or 'evan-all') + * @param {String} path: URL to the base directory containing the Realtime plugin, + * used to fetch resources if needed. + * + * @todo timeline and path parameters are unused and probably should be removed. + * + * @access private + */ + initActions: function(url, timeline, path, keepaliveurl, closeurl) + { + $('#notices_primary').prepend('
    '); + + RealtimeUpdate._pluginPath = path; + RealtimeUpdate._keepaliveurl = keepaliveurl; + RealtimeUpdate._closeurl = closeurl; + + + // On unload, let the server know we're no longer listening + $(window).unload(function() { + $.ajax({ + type: 'POST', + url: RealtimeUpdate._closeurl}); + }); + + setInterval(function() { + $.ajax({ + type: 'POST', + url: RealtimeUpdate._keepaliveurl}); + + }, 15 * 60 * 1000 ); // every 15 min; timeout in 30 min + + RealtimeUpdate.initPlayPause(); + RealtimeUpdate.initAddPopup(url, timeline, RealtimeUpdate._pluginPath); + }, + + /** + * Initialize the state of the play/pause controls. + * + * If the browser supports the localStorage interface, we'll attempt + * to retrieve a pause state from there; otherwise we default to paused. + * + * @access private + */ + initPlayPause: function() + { + if (typeof(localStorage) == 'undefined') { + RealtimeUpdate.showPause(); + } + else { + if (localStorage.getItem('RealtimeUpdate_paused') === 'true') { + RealtimeUpdate.showPlay(); + } + else { + RealtimeUpdate.showPause(); + } + } + }, + + /** + * Switch the realtime UI into paused state. + * Uses SN.msg i18n system for the button label and tooltip. + * + * State will be saved and re-used next time if the browser supports + * the localStorage interface (via setPause). + * + * @access private + */ + showPause: function() + { + RealtimeUpdate.setPause(false); + RealtimeUpdate.showQueuedNotices(); + RealtimeUpdate.addNoticesHover(); + + $('#realtime_playpause').remove(); + $('#realtime_actions').prepend('
  • '); + $('#realtime_pause').text(SN.msg('realtime_pause')) + .attr('title', SN.msg('realtime_pause_tooltip')) + .bind('click', function() { + RealtimeUpdate.removeNoticesHover(); + RealtimeUpdate.showPlay(); + return false; + }); + }, + + /** + * Switch the realtime UI into play state. + * Uses SN.msg i18n system for the button label and tooltip. + * + * State will be saved and re-used next time if the browser supports + * the localStorage interface (via setPause). + * + * @access private + */ + showPlay: function() + { + RealtimeUpdate.setPause(true); + $('#realtime_playpause').remove(); + $('#realtime_actions').prepend('
  • '); + $('#realtime_play').text(SN.msg('realtime_play')) + .attr('title', SN.msg('realtime_play_tooltip')) + .bind('click', function() { + RealtimeUpdate.showPause(); + return false; + }); + }, + + /** + * Update the internal pause/play state. + * Do not call directly; use showPause() and showPlay(). + * + * State will be saved and re-used next time if the browser supports + * the localStorage interface. + * + * @param {boolean} state: true = paused, false = not paused + * + * @access private + */ + setPause: function(state) + { + RealtimeUpdate._paused = state; + if (typeof(localStorage) != 'undefined') { + localStorage.setItem('RealtimeUpdate_paused', RealtimeUpdate._paused); + } + }, + + /** + * Go through notices we have previously received while paused, + * dumping them into the timeline view. + * + * @fixme long timelines are not trimmed here as they are for things received while not paused + * + * @access private + */ + showQueuedNotices: function() + { + $.each(RealtimeUpdate._queuedNotices, function(i, n) { + RealtimeUpdate.insertNoticeItem(n); + }); + + RealtimeUpdate._queuedNotices = []; + + RealtimeUpdate.removeQueuedCounter(); + }, + + /** + * Update the Realtime widget control's counter of queued notices to show + * the current count. This will be called after receiving and queueing + * a notice while paused. + * + * @access private + */ + updateQueuedCounter: function() + { + $('#realtime_playpause #queued_counter').html('('+RealtimeUpdate._queuedNotices.length+')'); + }, + + /** + * Clear the Realtime widget control's counter of queued notices. + * + * @access private + */ + removeQueuedCounter: function() + { + $('#realtime_playpause #queued_counter').empty(); + }, + + /** + * Set up event handlers on the timeline view to automatically pause + * when the mouse is over the timeline, as this indicates the user's + * desire to interact with the UI. (Which is hard to do when it's moving!) + * + * @access private + */ + addNoticesHover: function() + { + $('#notices_primary .notices').hover( + function() { + if (RealtimeUpdate._paused === false) { + RealtimeUpdate.showPlay(); + } + }, + function() { + if (RealtimeUpdate._paused === true) { + RealtimeUpdate.showPause(); + } + } + ); + }, + + /** + * Tear down event handlers on the timeline view to automatically pause + * when the mouse is over the timeline. + * + * @fixme this appears to remove *ALL* event handlers from the timeline, + * which assumes that nobody else is adding any event handlers. + * Sloppy -- we should only remove the ones we add. + * + * @access private + */ + removeNoticesHover: function() + { + $('#notices_primary .notices').unbind(); + }, + + /** + * UI initialization, to be called from Realtime plugin code on regular + * timeline pages. + * + * Adds a button to the control widget at the top of the timeline view, + * allowing creation of a popup window with a more compact real-time + * view of the current timeline. + * + * @param {String} url: full URL to the popup window variant of this timeline page + * @param {String} timeline: string key for the timeline (eg 'public' or 'evan-all') + * @param {String} path: URL to the base directory containing the Realtime plugin, + * used to fetch resources if needed. + * + * @todo timeline and path parameters are unused and probably should be removed. + * + * @access public + */ + initAddPopup: function(url, timeline, path) + { + $('#realtime_timeline').append(''); + $('#realtime_popup').text(SN.msg('realtime_popup')) + .attr('title', SN.msg('realtime_popup_tooltip')) + .bind('click', function() { + window.open(url, + '', + 'toolbar=no,resizable=yes,scrollbars=yes,status=no,menubar=no,personalbar=no,location=no,width=500,height=550'); + + return false; + }); + }, + + /** + * UI initialization, to be called from Realtime plugin code on popup + * compact timeline pages. + * + * Sets up links in notices to open in a new window. + * + * @fixme fails to do the same for UI links like context view which will + * look bad in the tiny chromeless window. + * + * @access public + */ + initPopupWindow: function() + { + $('.notices .entry-title a, .notices .entry-content a').bind('click', function() { + window.open(this.href, ''); + + return false; + }); + + $('#showstream .entity_profile').css({'width':'69%'}); + } +} + diff --git a/plugins/Realtime/realtimeupdate.css b/plugins/Realtime/realtimeupdate.css deleted file mode 100644 index 3295fe4a31..0000000000 --- a/plugins/Realtime/realtimeupdate.css +++ /dev/null @@ -1,76 +0,0 @@ -.realtime-popup address { -display:none; -} - -.realtime-popup #content { -width:93.5%; -} - -.realtime-popup #form_notice { -margin:18px 0 18px 1.795%; -width:93%; -max-width:451px; -} - -.realtime-popup #form_notice label[for=notice_data-text], -.realtime-popup h1 { -display:none; -} - -.realtime-popup #form_notice label.notice_data-attach, -.realtime-popup #form_notice input.notice_data-attach, -.realtime-popup #form_notice label.notice_data-geo { -top:0; -} - -.realtime-popup #form_notice input.notice_data-attach { -left:auto; -right:0; -} - -.realtime-popup .entity_profile { -width:70%; -} -.realtime-popup .entity_actions { -margin-left:1%; -} - -#notices_primary { -position:relative; -} - -#realtime_actions { -position: absolute; -top: -20px; -right: 0; -margin: 0 0 11px 0; -} - -#realtime_actions li { -margin-left: 18px; -list-style-type: none; -float: left; -} - -#realtime_actions button { -width: 16px; -height: 16px; -display: block; -border: none; -cursor: pointer; -text-indent: -9999px; -float: left; -} - -#realtime_play { -margin-left: 4px; -} - -#queued_counter { -float:left; -line-height:1.2; -} - -#showstream #notices_primary { -margin-top: 18px; -} diff --git a/plugins/Realtime/realtimeupdate.js b/plugins/Realtime/realtimeupdate.js deleted file mode 100644 index 90d0a05b09..0000000000 --- a/plugins/Realtime/realtimeupdate.js +++ /dev/null @@ -1,644 +0,0 @@ -/* - * StatusNet - a distributed open-source microblogging tool - * Copyright (C) 2009-2011, StatusNet, Inc. - * - * Add a notice encoded as JSON into the current timeline - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * - * @category Plugin - * @package StatusNet - * @author Evan Prodromou - * @author Sarven Capadisli - * @copyright 2009-2011 StatusNet, Inc. - * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 - * @link http://status.net/ - */ - -/** - * This is the UI portion of the Realtime plugin base class, handling - * queueing up and displaying of notices that have been received through - * other code in one of the subclassed plugin implementations such as - * Meteor or Orbited. - * - * Notices are passed in as JSON objects formatted per the Twitter-compatible - * API. - * - * @todo Currently we duplicate a lot of formatting and layout code from - * the PHP side of StatusNet, which makes it very difficult to maintain - * this package. Internationalization as well as newer features such - * as location data, customized source links for OStatus profiles, - * and image thumbnails are not yet supported in Realtime yet because - * they have not been implemented here. - */ -RealtimeUpdate = { - _userid: 0, - _showurl: '', - _keepaliveurl: '', - _closeurl: '', - _updatecounter: 0, - _maxnotices: 50, - _windowhasfocus: true, - _documenttitle: '', - _paused:false, - _queuedNotices:[], - - /** - * Initialize the Realtime plugin UI on a page with a timeline view. - * - * This function is called from a JS fragment inserted by the PHP side - * of the Realtime plugin, and provides us with base information - * needed to build a near-replica of StatusNet's NoticeListItem output. - * - * Once the UI is initialized, a plugin subclass will need to actually - * feed data into the RealtimeUpdate object! - * - * @param {int} userid: local profile ID of the currently logged-in user - * @param {String} showurl: URL for shownotice action, used when fetching formatting notices. - * This URL contains a stub value of 0000000000 which will be replaced with the notice ID. - * - * @access public - */ - init: function(userid, showurl) - { - RealtimeUpdate._userid = userid; - RealtimeUpdate._showurl = showurl; - - RealtimeUpdate._documenttitle = document.title; - - $(window).bind('focus', function() { - RealtimeUpdate._windowhasfocus = true; - - // Clear the counter on the window title when we focus in. - RealtimeUpdate._updatecounter = 0; - RealtimeUpdate.removeWindowCounter(); - }); - - $(window).bind('blur', function() { - $('#notices_primary .notice').removeClass('mark-top'); - - $('#notices_primary .notice:first').addClass('mark-top'); - - // While we're in the background, received messages will increment - // a counter that we put on the window title. This will cause some - // browsers to also flash or mark the tab or window title bar until - // you seek attention (eg Firefox 4 pinned app tabs). - RealtimeUpdate._windowhasfocus = false; - - return false; - }); - }, - - /** - * Accept a notice in a Twitter-API JSON style and either show it - * or queue it up, depending on whether the realtime display is - * active. - * - * The meat of a Realtime plugin subclass is to provide a substrate - * transport to receive data and shove it into this function. :) - * - * Note that the JSON data is extended from the standard API return - * with additional fields added by RealtimePlugin's PHP code. - * - * @param {Object} data: extended JSON API-formatted notice - * - * @access public - */ - receive: function(data) - { - if (RealtimeUpdate.isNoticeVisible(data.id)) { - // Probably posted by the user in this window, and so already - // shown by the AJAX form handler. Ignore it. - return; - } - if (RealtimeUpdate._paused === false) { - RealtimeUpdate.purgeLastNoticeItem(); - - RealtimeUpdate.insertNoticeItem(data); - } - else { - RealtimeUpdate._queuedNotices.push(data); - - RealtimeUpdate.updateQueuedCounter(); - } - - RealtimeUpdate.updateWindowCounter(); - }, - - /** - * Add a visible representation of the given notice at the top of - * the current timeline. - * - * If the notice is already in the timeline, nothing will be added. - * - * @param {Object} data: extended JSON API-formatted notice - * - * @fixme while core UI JS code is used to activate the AJAX UI controls, - * the actual production of HTML (in makeNoticeItem and its subs) - * duplicates core code without plugin hook points or i18n support. - * - * @access private - */ - insertNoticeItem: function(data) { - // Don't add it if it already exists - if (RealtimeUpdate.isNoticeVisible(data.id)) { - return; - } - - RealtimeUpdate.makeNoticeItem(data, function(noticeItem) { - // Check again in case it got shown while we were waiting for data... - if (RealtimeUpdate.isNoticeVisible(data.id)) { - return; - } - var noticeItemID = $(noticeItem).attr('id'); - - var list = $("#notices_primary .notices:first") - var prepend = true; - - var threaded = list.hasClass('threaded-notices'); - if (threaded && data.in_reply_to_status_id) { - // aho! - var parent = $('#notice-' + data.in_reply_to_status_id); - if (parent.length == 0) { - // @todo fetch the original, insert it, and finish the rest - } else { - // Check the parent notice to make sure it's not a reply itself. - // If so, use it's parent as the parent. - var parentList = parent.closest('.notices'); - if (parentList.hasClass('threaded-replies')) { - parent = parentList.closest('.notice'); - } - list = parent.find('.threaded-replies'); - if (list.length == 0) { - list = $('
      '); - parent.append(list); - SN.U.NoticeInlineReplyPlaceholder(parent); - } - prepend = false; - } - } - - var newNotice = $(noticeItem); - if (prepend) { - list.prepend(newNotice); - } else { - var placeholder = list.find('li.notice-reply-placeholder') - if (placeholder.length > 0) { - newNotice.insertBefore(placeholder) - } else { - newNotice.appendTo(list); - } - } - newNotice.css({display:"none"}).fadeIn(1000); - - SN.U.NoticeReplyTo($('#'+noticeItemID)); - SN.U.NoticeWithAttachment($('#'+noticeItemID)); - }); - }, - - /** - * Check if the given notice is visible in the timeline currently. - * Used to avoid duplicate processing of notices that have been - * displayed by other means. - * - * @param {number} id: notice ID to check - * - * @return boolean - * - * @access private - */ - isNoticeVisible: function(id) { - return ($("#notice-"+id).length > 0); - }, - - /** - * Trims a notice off the end of the timeline if we have more than the - * maximum number of notices visible. - * - * @access private - */ - purgeLastNoticeItem: function() { - if ($('#notices_primary .notice').length > RealtimeUpdate._maxnotices) { - $("#notices_primary .notice:last").remove(); - } - }, - - /** - * If the window/tab is in background, increment the counter of newly - * received notices and append it onto the window title. - * - * Has no effect if the window is in foreground. - * - * @access private - */ - updateWindowCounter: function() { - if (RealtimeUpdate._windowhasfocus === false) { - RealtimeUpdate._updatecounter += 1; - document.title = '('+RealtimeUpdate._updatecounter+') ' + RealtimeUpdate._documenttitle; - } - }, - - /** - * Clear the background update counter from the window title. - * - * @access private - * - * @fixme could interfere with anything else trying similar tricks - */ - removeWindowCounter: function() { - document.title = RealtimeUpdate._documenttitle; - }, - - /** - * Builds a notice HTML block from JSON API-style data; - * loads data from server, so runs async. - * - * @param {Object} data: extended JSON API-formatted notice - * @param {function} callback: function(DOMNode) to receive new code - * - * @access private - */ - makeNoticeItem: function(data, callback) - { - var url = RealtimeUpdate._showurl.replace('0000000000', data.id); - $.get(url, {ajax: 1}, function(data, textStatus, xhr) { - var notice = $('li.notice:first', data); - if (notice.length) { - var node = document._importNode(notice[0], true); - callback(node); - } - }); - }, - - /** - * Creates a favorite button. - * - * @param {number} id: notice ID to work with - * @param {String} session_key: session token for form CSRF protection - * @return {String} HTML fragment - * - * @fixme this replicates core StatusNet code, making maintenance harder - * @fixme sloppy HTML building (raw concat without escaping) - * @fixme no i18n support - * - * @access private - */ - makeFavoriteForm: function(id, session_key) - { - var ff; - - ff = "
      "+ - "
      "+ - "Favor this notice"+ - ""+ - ""+ - ""+ - "
      "+ - "
      "; - return ff; - }, - - /** - * Creates a reply button. - * - * @param {number} id: notice ID to work with - * @param {String} nickname: nick of the user to whom we are replying - * @return {String} HTML fragment - * - * @fixme this replicates core StatusNet code, making maintenance harder - * @fixme sloppy HTML building (raw concat without escaping) - * @fixme no i18n support - * - * @access private - */ - makeReplyLink: function(id, nickname) - { - var rl; - rl = "Reply "+id+""; - return rl; - }, - - /** - * Creates a repeat button. - * - * @param {number} id: notice ID to work with - * @param {String} session_key: session token for form CSRF protection - * @return {String} HTML fragment - * - * @fixme this replicates core StatusNet code, making maintenance harder - * @fixme sloppy HTML building (raw concat without escaping) - * @fixme no i18n support - * - * @access private - */ - makeRepeatForm: function(id, session_key) - { - var rf; - rf = "
      "+ - "
      "+ - "Repeat this notice?"+ - ""+ - ""+ - ""+ - "
      "+ - "
      "; - - return rf; - }, - - /** - * Creates a delete button. - * - * @param {number} id: notice ID to create a delete link for - * @return {String} HTML fragment - * - * @fixme this replicates core StatusNet code, making maintenance harder - * @fixme sloppy HTML building (raw concat without escaping) - * @fixme no i18n support - * - * @access private - */ - makeDeleteLink: function(id) - { - var dl, delurl; - delurl = RealtimeUpdate._deleteurl.replace("0000000000", id); - - dl = "Delete"; - - return dl; - }, - - /** - * Adds a control widget at the top of the timeline view, containing - * pause/play and popup buttons. - * - * @param {String} url: full URL to the popup window variant of this timeline page - * @param {String} timeline: string key for the timeline (eg 'public' or 'evan-all') - * @param {String} path: URL to the base directory containing the Realtime plugin, - * used to fetch resources if needed. - * - * @todo timeline and path parameters are unused and probably should be removed. - * - * @access private - */ - initActions: function(url, timeline, path, keepaliveurl, closeurl) - { - $('#notices_primary').prepend('
      '); - - RealtimeUpdate._pluginPath = path; - RealtimeUpdate._keepaliveurl = keepaliveurl; - RealtimeUpdate._closeurl = closeurl; - - - // On unload, let the server know we're no longer listening - $(window).unload(function() { - $.ajax({ - type: 'POST', - url: RealtimeUpdate._closeurl}); - }); - - setInterval(function() { - $.ajax({ - type: 'POST', - url: RealtimeUpdate._keepaliveurl}); - - }, 15 * 60 * 1000 ); // every 15 min; timeout in 30 min - - RealtimeUpdate.initPlayPause(); - RealtimeUpdate.initAddPopup(url, timeline, RealtimeUpdate._pluginPath); - }, - - /** - * Initialize the state of the play/pause controls. - * - * If the browser supports the localStorage interface, we'll attempt - * to retrieve a pause state from there; otherwise we default to paused. - * - * @access private - */ - initPlayPause: function() - { - if (typeof(localStorage) == 'undefined') { - RealtimeUpdate.showPause(); - } - else { - if (localStorage.getItem('RealtimeUpdate_paused') === 'true') { - RealtimeUpdate.showPlay(); - } - else { - RealtimeUpdate.showPause(); - } - } - }, - - /** - * Switch the realtime UI into paused state. - * Uses SN.msg i18n system for the button label and tooltip. - * - * State will be saved and re-used next time if the browser supports - * the localStorage interface (via setPause). - * - * @access private - */ - showPause: function() - { - RealtimeUpdate.setPause(false); - RealtimeUpdate.showQueuedNotices(); - RealtimeUpdate.addNoticesHover(); - - $('#realtime_playpause').remove(); - $('#realtime_actions').prepend('
    • '); - $('#realtime_pause').text(SN.msg('realtime_pause')) - .attr('title', SN.msg('realtime_pause_tooltip')) - .bind('click', function() { - RealtimeUpdate.removeNoticesHover(); - RealtimeUpdate.showPlay(); - return false; - }); - }, - - /** - * Switch the realtime UI into play state. - * Uses SN.msg i18n system for the button label and tooltip. - * - * State will be saved and re-used next time if the browser supports - * the localStorage interface (via setPause). - * - * @access private - */ - showPlay: function() - { - RealtimeUpdate.setPause(true); - $('#realtime_playpause').remove(); - $('#realtime_actions').prepend('
    • '); - $('#realtime_play').text(SN.msg('realtime_play')) - .attr('title', SN.msg('realtime_play_tooltip')) - .bind('click', function() { - RealtimeUpdate.showPause(); - return false; - }); - }, - - /** - * Update the internal pause/play state. - * Do not call directly; use showPause() and showPlay(). - * - * State will be saved and re-used next time if the browser supports - * the localStorage interface. - * - * @param {boolean} state: true = paused, false = not paused - * - * @access private - */ - setPause: function(state) - { - RealtimeUpdate._paused = state; - if (typeof(localStorage) != 'undefined') { - localStorage.setItem('RealtimeUpdate_paused', RealtimeUpdate._paused); - } - }, - - /** - * Go through notices we have previously received while paused, - * dumping them into the timeline view. - * - * @fixme long timelines are not trimmed here as they are for things received while not paused - * - * @access private - */ - showQueuedNotices: function() - { - $.each(RealtimeUpdate._queuedNotices, function(i, n) { - RealtimeUpdate.insertNoticeItem(n); - }); - - RealtimeUpdate._queuedNotices = []; - - RealtimeUpdate.removeQueuedCounter(); - }, - - /** - * Update the Realtime widget control's counter of queued notices to show - * the current count. This will be called after receiving and queueing - * a notice while paused. - * - * @access private - */ - updateQueuedCounter: function() - { - $('#realtime_playpause #queued_counter').html('('+RealtimeUpdate._queuedNotices.length+')'); - }, - - /** - * Clear the Realtime widget control's counter of queued notices. - * - * @access private - */ - removeQueuedCounter: function() - { - $('#realtime_playpause #queued_counter').empty(); - }, - - /** - * Set up event handlers on the timeline view to automatically pause - * when the mouse is over the timeline, as this indicates the user's - * desire to interact with the UI. (Which is hard to do when it's moving!) - * - * @access private - */ - addNoticesHover: function() - { - $('#notices_primary .notices').hover( - function() { - if (RealtimeUpdate._paused === false) { - RealtimeUpdate.showPlay(); - } - }, - function() { - if (RealtimeUpdate._paused === true) { - RealtimeUpdate.showPause(); - } - } - ); - }, - - /** - * Tear down event handlers on the timeline view to automatically pause - * when the mouse is over the timeline. - * - * @fixme this appears to remove *ALL* event handlers from the timeline, - * which assumes that nobody else is adding any event handlers. - * Sloppy -- we should only remove the ones we add. - * - * @access private - */ - removeNoticesHover: function() - { - $('#notices_primary .notices').unbind(); - }, - - /** - * UI initialization, to be called from Realtime plugin code on regular - * timeline pages. - * - * Adds a button to the control widget at the top of the timeline view, - * allowing creation of a popup window with a more compact real-time - * view of the current timeline. - * - * @param {String} url: full URL to the popup window variant of this timeline page - * @param {String} timeline: string key for the timeline (eg 'public' or 'evan-all') - * @param {String} path: URL to the base directory containing the Realtime plugin, - * used to fetch resources if needed. - * - * @todo timeline and path parameters are unused and probably should be removed. - * - * @access public - */ - initAddPopup: function(url, timeline, path) - { - $('#realtime_timeline').append(''); - $('#realtime_popup').text(SN.msg('realtime_popup')) - .attr('title', SN.msg('realtime_popup_tooltip')) - .bind('click', function() { - window.open(url, - '', - 'toolbar=no,resizable=yes,scrollbars=yes,status=no,menubar=no,personalbar=no,location=no,width=500,height=550'); - - return false; - }); - }, - - /** - * UI initialization, to be called from Realtime plugin code on popup - * compact timeline pages. - * - * Sets up links in notices to open in a new window. - * - * @fixme fails to do the same for UI links like context view which will - * look bad in the tiny chromeless window. - * - * @access public - */ - initPopupWindow: function() - { - $('.notices .entry-title a, .notices .entry-content a').bind('click', function() { - window.open(this.href, ''); - - return false; - }); - - $('#showstream .entity_profile').css({'width':'69%'}); - } -} - diff --git a/plugins/Realtime/realtimeupdate.min.js b/plugins/Realtime/realtimeupdate.min.js deleted file mode 100644 index ad3fb97a76..0000000000 --- a/plugins/Realtime/realtimeupdate.min.js +++ /dev/null @@ -1 +0,0 @@ -RealtimeUpdate={_userid:0,_showurl:"",_keepaliveurl:"",_closeurl:"",_updatecounter:0,_maxnotices:50,_windowhasfocus:true,_documenttitle:"",_paused:false,_queuedNotices:[],init:function(a,b){RealtimeUpdate._userid=a;RealtimeUpdate._showurl=b;RealtimeUpdate._documenttitle=document.title;$(window).bind("focus",function(){RealtimeUpdate._windowhasfocus=true;RealtimeUpdate._updatecounter=0;RealtimeUpdate.removeWindowCounter()});$(window).bind("blur",function(){$("#notices_primary .notice").removeClass("mark-top");$("#notices_primary .notice:first").addClass("mark-top");RealtimeUpdate._windowhasfocus=false;return false})},receive:function(a){if(RealtimeUpdate.isNoticeVisible(a.id)){return}if(RealtimeUpdate._paused===false){RealtimeUpdate.purgeLastNoticeItem();RealtimeUpdate.insertNoticeItem(a)}else{RealtimeUpdate._queuedNotices.push(a);RealtimeUpdate.updateQueuedCounter()}RealtimeUpdate.updateWindowCounter()},insertNoticeItem:function(a){if(RealtimeUpdate.isNoticeVisible(a.id)){return}RealtimeUpdate.makeNoticeItem(a,function(b){if(RealtimeUpdate.isNoticeVisible(a.id)){return}var c=$(b).attr("id");var d=$("#notices_primary .notices:first");var j=true;var e=d.hasClass("threaded-notices");if(e&&a.in_reply_to_status_id){var g=$("#notice-"+a.in_reply_to_status_id);if(g.length==0){}else{var h=g.closest(".notices");if(h.hasClass("threaded-replies")){g=h.closest(".notice")}d=g.find(".threaded-replies");if(d.length==0){d=$('
        ');g.append(d);SN.U.NoticeInlineReplyPlaceholder(g)}j=false}}var i=$(b);if(j){d.prepend(i)}else{var f=d.find("li.notice-reply-placeholder");if(f.length>0){i.insertBefore(f)}else{i.appendTo(d)}}i.css({display:"none"}).fadeIn(1000);SN.U.NoticeReplyTo($("#"+c));SN.U.NoticeWithAttachment($("#"+c))})},isNoticeVisible:function(a){return($("#notice-"+a).length>0)},purgeLastNoticeItem:function(){if($("#notices_primary .notice").length>RealtimeUpdate._maxnotices){$("#notices_primary .notice:last").remove()}},updateWindowCounter:function(){if(RealtimeUpdate._windowhasfocus===false){RealtimeUpdate._updatecounter+=1;document.title="("+RealtimeUpdate._updatecounter+") "+RealtimeUpdate._documenttitle}},removeWindowCounter:function(){document.title=RealtimeUpdate._documenttitle},makeNoticeItem:function(b,c){var a=RealtimeUpdate._showurl.replace("0000000000",b.id);$.get(a,{ajax:1},function(f,h,g){var e=$("li.notice:first",f);if(e.length){var d=document._importNode(e[0],true);c(d)}})},makeFavoriteForm:function(c,b){var a;a='
        Favor this notice
        ';return a},makeReplyLink:function(c,a){var b;b='Reply '+c+"";return b},makeRepeatForm:function(c,b){var a;a='
        Repeat this notice?
        ';return a},makeDeleteLink:function(c){var b,a;a=RealtimeUpdate._deleteurl.replace("0000000000",c);b='Delete';return b},initActions:function(a,c,d,b,e){$("#notices_primary").prepend('
        ');RealtimeUpdate._pluginPath=d;RealtimeUpdate._keepaliveurl=b;RealtimeUpdate._closeurl=e;$(window).unload(function(){$.ajax({type:"POST",url:RealtimeUpdate._closeurl})});setInterval(function(){$.ajax({type:"POST",url:RealtimeUpdate._keepaliveurl})},15*60*1000);RealtimeUpdate.initPlayPause();RealtimeUpdate.initAddPopup(a,c,RealtimeUpdate._pluginPath)},initPlayPause:function(){if(typeof(localStorage)=="undefined"){RealtimeUpdate.showPause()}else{if(localStorage.getItem("RealtimeUpdate_paused")==="true"){RealtimeUpdate.showPlay()}else{RealtimeUpdate.showPause()}}},showPause:function(){RealtimeUpdate.setPause(false);RealtimeUpdate.showQueuedNotices();RealtimeUpdate.addNoticesHover();$("#realtime_playpause").remove();$("#realtime_actions").prepend('
      • ');$("#realtime_pause").text(SN.msg("realtime_pause")).attr("title",SN.msg("realtime_pause_tooltip")).bind("click",function(){RealtimeUpdate.removeNoticesHover();RealtimeUpdate.showPlay();return false})},showPlay:function(){RealtimeUpdate.setPause(true);$("#realtime_playpause").remove();$("#realtime_actions").prepend('
      • ');$("#realtime_play").text(SN.msg("realtime_play")).attr("title",SN.msg("realtime_play_tooltip")).bind("click",function(){RealtimeUpdate.showPause();return false})},setPause:function(a){RealtimeUpdate._paused=a;if(typeof(localStorage)!="undefined"){localStorage.setItem("RealtimeUpdate_paused",RealtimeUpdate._paused)}},showQueuedNotices:function(){$.each(RealtimeUpdate._queuedNotices,function(a,b){RealtimeUpdate.insertNoticeItem(b)});RealtimeUpdate._queuedNotices=[];RealtimeUpdate.removeQueuedCounter()},updateQueuedCounter:function(){$("#realtime_playpause #queued_counter").html("("+RealtimeUpdate._queuedNotices.length+")")},removeQueuedCounter:function(){$("#realtime_playpause #queued_counter").empty()},addNoticesHover:function(){$("#notices_primary .notices").hover(function(){if(RealtimeUpdate._paused===false){RealtimeUpdate.showPlay()}},function(){if(RealtimeUpdate._paused===true){RealtimeUpdate.showPause()}})},removeNoticesHover:function(){$("#notices_primary .notices").unbind()},initAddPopup:function(a,b,c){$("#realtime_timeline").append('');$("#realtime_popup").text(SN.msg("realtime_popup")).attr("title",SN.msg("realtime_popup_tooltip")).bind("click",function(){window.open(a,"","toolbar=no,resizable=yes,scrollbars=yes,status=no,menubar=no,personalbar=no,location=no,width=500,height=550");return false})},initPopupWindow:function(){$(".notices .entry-title a, .notices .entry-content a").bind("click",function(){window.open(this.href,"");return false});$("#showstream .entity_profile").css({width:"69%"})}}; \ No newline at end of file