]> git.mxchange.org Git - quix0rs-gnu-social.git/commitdiff
Preparing plugins for no-minify-in-core-policy
authorMikael Nordfeldth <mmn@hethane.se>
Sun, 23 Feb 2014 23:59:29 +0000 (00:59 +0100)
committerMikael Nordfeldth <mmn@hethane.se>
Mon, 24 Feb 2014 00:01:34 +0000 (01:01 +0100)
also making the file structure better with js and css folders for Realtime and LinkPreview

26 files changed:
plugins/Comet/CometPlugin.php
plugins/Comet/bayeux.class.inc.php [deleted file]
plugins/Comet/cometupdate.js [deleted file]
plugins/Comet/extlib/Bayeux/Bayeux.class.php [new file with mode: 0644]
plugins/Comet/jquery.comet.js [deleted file]
plugins/Comet/js/cometupdate.js [new file with mode: 0644]
plugins/Comet/js/jquery.comet.js [new file with mode: 0644]
plugins/LinkPreview/LinkPreviewPlugin.php
plugins/LinkPreview/js/linkpreview.js [new file with mode: 0644]
plugins/LinkPreview/linkpreview.js [deleted file]
plugins/LinkPreview/linkpreview.min.js [deleted file]
plugins/Meteor/MeteorPlugin.php
plugins/Meteor/js/meteorupdater.js [new file with mode: 0644]
plugins/Meteor/meteorupdater.js [deleted file]
plugins/Meteor/meteorupdater.min.js [deleted file]
plugins/Orbited/OrbitedPlugin.php
plugins/Orbited/js/orbitedextra.js [new file with mode: 0644]
plugins/Orbited/js/orbitedupdater.js [new file with mode: 0644]
plugins/Orbited/orbitedextra.js [deleted file]
plugins/Orbited/orbitedupdater.js [deleted file]
plugins/Realtime/RealtimePlugin.php
plugins/Realtime/css/realtimeupdate.css [new file with mode: 0644]
plugins/Realtime/js/realtimeupdate.js [new file with mode: 0644]
plugins/Realtime/realtimeupdate.css [deleted file]
plugins/Realtime/realtimeupdate.js [deleted file]
plugins/Realtime/realtimeupdate.min.js [deleted file]

index 70f5ab85fe45ac0e59404cac9b9eb734eebb5753..22f791e2fccb032270458eb27d970fc348a63d2c 100644 (file)
  * @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 (file)
index 4fe8600..0000000
+++ /dev/null
@@ -1,133 +0,0 @@
-<?php
-/*
- * Phomet: a php comet client
- *
- * Copyright (C) 2008 Morgan 'ARR!' Allen <morganrallen@gmail.com> 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 (file)
index 50b02b7..0000000
+++ /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 (file)
index 0000000..4fe8600
--- /dev/null
@@ -0,0 +1,133 @@
+<?php
+/*
+ * Phomet: a php comet client
+ *
+ * Copyright (C) 2008 Morgan 'ARR!' Allen <morganrallen@gmail.com> 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 (file)
index 6de437f..0000000
+++ /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 <code>$.cometd</code>,
-     * 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:
-     * <pre>
-     * var url2 = ...;
-     * var cometd2 = new $.Cometd();
-     * cometd2.init(url2);
-     * </pre>
-     */
-    $.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<channel, subscription[]>, 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:
-         * <pre>
-         * {
-         *     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.
-         * </pre>
-         * @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 (file)
index 0000000..50b02b7
--- /dev/null
@@ -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 (file)
index 0000000..6de437f
--- /dev/null
@@ -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 <code>$.cometd</code>,
+     * 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:
+     * <pre>
+     * var url2 = ...;
+     * var cometd2 = new $.Cometd();
+     * cometd2.init(url2);
+     * </pre>
+     */
+    $.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<channel, subscription[]>, 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:
+         * <pre>
+         * {
+         *     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.
+         * </pre>
+         * @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);
index 301076ec94e7cb9658df0a86f2f4bac87be64e95..0a1d6d0da8b0986f4d6702be650e4f5f69727368 100644 (file)
@@ -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 (file)
index 0000000..e6e98bd
--- /dev/null
@@ -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('<div class="notice-status link-preview thumbnails"></div>');
+                }
+            },
+
+            /**
+             * 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 = $('<span class="inline-attachment"><a><img/></a></span>');
+                        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('<span class="' + id + '"></span>');
+                }
+            },
+
+            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 (file)
index e6e98bd..0000000
+++ /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('<div class="notice-status link-preview thumbnails"></div>');
-                }
-            },
-
-            /**
-             * 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 = $('<span class="inline-attachment"><a><img/></a></span>');
-                        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('<span class="' + id + '"></span>');
-                }
-            },
-
-            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 (file)
index 99af839..0000000
+++ /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<g.length;f++){g[f](h)}})}else{b.callbacks[d].push(e)}}},rawLookup:function(d,f){var e={url:d,format:"json",maxwidth:b.width,maxheight:b.height,token:$("#token").val()};$.ajax({url:b.api,data:e,dataType:"json",success:function(g,h){f(g)},error:function(h,i,g){f(null)}})}};SN.Init.LinkPreview=function(d){if(d.api){b.api=d.api}if(d.width){b.width=d.width}if(d.height){b.height=d.height}};var c=SN.U.Counter;SN.U.Counter=function(d){var e=d.data("LinkPreview");if(e){e.previewLinks(d.find(".notice_data-text:first").val())}return c(d)};var a=SN.Init.NoticeFormSetup;SN.Init.NoticeFormSetup=function(d){a(d);d.bind("reset",function(){e.clear()});var e={links:[],state:[],refresh:[],findLinks:function(i){var g=/(?:^| )(https?:\/\/.+?\/.+?)(?= |$)/mg;var f=[];var h;while((h=g.exec(i))!==null){f.push(h[1])}return f},ensureArea:function(){if(d.find(".link-preview").length<1){d.append('<div class="notice-status link-preview thumbnails"></div>')}},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<j){j=l.thumbnail_width}}}else{if(l&&l.type=="photo"&&typeof l.url=="string"){i=l.url;if(typeof l.width!=="undefined"){if(l.width<j){j=l.width}}}}if(i){e.ensureArea();var k=$('<span class="inline-attachment"><a><img/></a></span>');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;h<f.length&&h<g.length;h++){if(g[h]!=f[h]){if(e.state[h]=="loading"){e.refresh[h]=true}else{e.prepLinkPreview(h)}}}if(g.length>f.length){for(h=f.length;h<g.length;h++){e.addPreviewArea(h);e.prepLinkPreview(h)}}else{if(f.length>g.length){for(h=g.length;h<f.length;h++){e.clearLink(h)}}}if(g.length==0){e.clear()}},addPreviewArea:function(f){e.ensureArea();var g="link-preview-"+f;if(d.find("."+g).length<1){d.find(".link-preview").append('<span class="'+g+'"></span>')}},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
index d65fe3f0e51cd45a637d4c5ba6cc8806efde8341..b495fcddd94a54f242664ff3ceca0092c0a5e149 100644 (file)
  * @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 (file)
index 0000000..cdd1d63
--- /dev/null
@@ -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 (file)
index cdd1d63..0000000
+++ /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 (file)
index 61928ab..0000000
+++ /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
index 6ae394b61d49470df6cc808bbfbc088fc422c325..6267b38f7eefab0783e4442b0264eeebffbb2f90 100644 (file)
  * @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 (file)
index 0000000..47e5c0c
--- /dev/null
@@ -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 (file)
index 0000000..8c5ab3b
--- /dev/null
@@ -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 (file)
index 47e5c0c..0000000
+++ /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 (file)
index 8c5ab3b..0000000
+++ /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);
-          }
-     }
-}();
-
index 958716fa7621af0cf398e9f16f1263291da55b57..138ed4dc7aa7a38b56336921a9bd41d82b9b4d72 100644 (file)
@@ -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 (file)
index 0000000..3295fe4
--- /dev/null
@@ -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 (file)
index 0000000..90d0a05
--- /dev/null
@@ -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 <http://www.gnu.org/licenses/>.
+ *
+ * @category  Plugin
+ * @package   StatusNet
+ * @author    Evan Prodromou <evan@status.net>
+ * @author    Sarven Capadisli <csarven@status.net>
+ * @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 = $('<ul class="notices threaded-replies xoxo"></ul>');
+                        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 = "<form id=\"favor-"+id+"\" class=\"form_favor\" method=\"post\" action=\""+RealtimeUpdate._favorurl+"\">"+
+                "<fieldset>"+
+               "<legend>Favor this notice</legend>"+
+               "<input name=\"token\" type=\"hidden\" id=\"token-"+id+"\" value=\""+session_key+"\"/>"+
+               "<input name=\"notice\" type=\"hidden\" id=\"notice-n"+id+"\" value=\""+id+"\"/>"+
+               "<input type=\"submit\" id=\"favor-submit-"+id+"\" name=\"favor-submit-"+id+"\" class=\"submit\" value=\"Favor\" title=\"Favor this notice\"/>"+
+                "</fieldset>"+
+               "</form>";
+          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 = "<a class=\"notice_reply\" href=\""+RealtimeUpdate._replyurl+"?replyto="+nickname+"\" title=\"Reply to this notice\">Reply <span class=\"notice_id\">"+id+"</span></a>";
+          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 = "<form id=\"repeat-"+id+"\" class=\"form_repeat\" method=\"post\" action=\""+RealtimeUpdate._repeaturl+"\">"+
+               "<fieldset>"+
+               "<legend>Repeat this notice?</legend>"+
+               "<input name=\"token\" type=\"hidden\" id=\"token-"+id+"\" value=\""+session_key+"\"/>"+
+               "<input name=\"notice\" type=\"hidden\" id=\"notice-"+id+"\" value=\""+id+"\"/>"+
+               "<input type=\"submit\" id=\"repeat-submit-"+id+"\" name=\"repeat-submit-"+id+"\" class=\"submit\" value=\"Yes\" title=\"Repeat this notice\"/>"+
+               "</fieldset>"+
+               "</form>";
+
+          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 = "<a class=\"notice_delete\" href=\""+delurl+"\" title=\"Delete this notice\">Delete</a>";
+
+          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('<ul id="realtime_actions"><li id="realtime_playpause"></li><li id="realtime_timeline"></li></ul>');
+
+        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('<li id="realtime_playpause"><button id="realtime_pause" class="pause"></button></li>');
+        $('#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('<li id="realtime_playpause"><span id="queued_counter"></span> <button id="realtime_play" class="play"></button></li>');
+        $('#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('<button id="realtime_popup"></button>');
+         $('#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 (file)
index 3295fe4..0000000
+++ /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 (file)
index 90d0a05..0000000
+++ /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 <http://www.gnu.org/licenses/>.
- *
- * @category  Plugin
- * @package   StatusNet
- * @author    Evan Prodromou <evan@status.net>
- * @author    Sarven Capadisli <csarven@status.net>
- * @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 = $('<ul class="notices threaded-replies xoxo"></ul>');
-                        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 = "<form id=\"favor-"+id+"\" class=\"form_favor\" method=\"post\" action=\""+RealtimeUpdate._favorurl+"\">"+
-                "<fieldset>"+
-               "<legend>Favor this notice</legend>"+
-               "<input name=\"token\" type=\"hidden\" id=\"token-"+id+"\" value=\""+session_key+"\"/>"+
-               "<input name=\"notice\" type=\"hidden\" id=\"notice-n"+id+"\" value=\""+id+"\"/>"+
-               "<input type=\"submit\" id=\"favor-submit-"+id+"\" name=\"favor-submit-"+id+"\" class=\"submit\" value=\"Favor\" title=\"Favor this notice\"/>"+
-                "</fieldset>"+
-               "</form>";
-          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 = "<a class=\"notice_reply\" href=\""+RealtimeUpdate._replyurl+"?replyto="+nickname+"\" title=\"Reply to this notice\">Reply <span class=\"notice_id\">"+id+"</span></a>";
-          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 = "<form id=\"repeat-"+id+"\" class=\"form_repeat\" method=\"post\" action=\""+RealtimeUpdate._repeaturl+"\">"+
-               "<fieldset>"+
-               "<legend>Repeat this notice?</legend>"+
-               "<input name=\"token\" type=\"hidden\" id=\"token-"+id+"\" value=\""+session_key+"\"/>"+
-               "<input name=\"notice\" type=\"hidden\" id=\"notice-"+id+"\" value=\""+id+"\"/>"+
-               "<input type=\"submit\" id=\"repeat-submit-"+id+"\" name=\"repeat-submit-"+id+"\" class=\"submit\" value=\"Yes\" title=\"Repeat this notice\"/>"+
-               "</fieldset>"+
-               "</form>";
-
-          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 = "<a class=\"notice_delete\" href=\""+delurl+"\" title=\"Delete this notice\">Delete</a>";
-
-          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('<ul id="realtime_actions"><li id="realtime_playpause"></li><li id="realtime_timeline"></li></ul>');
-
-        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('<li id="realtime_playpause"><button id="realtime_pause" class="pause"></button></li>');
-        $('#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('<li id="realtime_playpause"><span id="queued_counter"></span> <button id="realtime_play" class="play"></button></li>');
-        $('#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('<button id="realtime_popup"></button>');
-         $('#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 (file)
index ad3fb97..0000000
+++ /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=$('<ul class="notices threaded-replies xoxo"></ul>');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='<form id="favor-'+c+'" class="form_favor" method="post" action="'+RealtimeUpdate._favorurl+'"><fieldset><legend>Favor this notice</legend><input name="token-'+c+'" type="hidden" id="token-'+c+'" value="'+b+'"/><input name="notice" type="hidden" id="notice-n'+c+'" value="'+c+'"/><input type="submit" id="favor-submit-'+c+'" name="favor-submit-'+c+'" class="submit" value="Favor" title="Favor this notice"/></fieldset></form>';return a},makeReplyLink:function(c,a){var b;b='<a class="notice_reply" href="'+RealtimeUpdate._replyurl+"?replyto="+a+'" title="Reply to this notice">Reply <span class="notice_id">'+c+"</span></a>";return b},makeRepeatForm:function(c,b){var a;a='<form id="repeat-'+c+'" class="form_repeat" method="post" action="'+RealtimeUpdate._repeaturl+'"><fieldset><legend>Repeat this notice?</legend><input name="token-'+c+'" type="hidden" id="token-'+c+'" value="'+b+'"/><input name="notice" type="hidden" id="notice-'+c+'" value="'+c+'"/><input type="submit" id="repeat-submit-'+c+'" name="repeat-submit-'+c+'" class="submit" value="Yes" title="Repeat this notice"/></fieldset></form>';return a},makeDeleteLink:function(c){var b,a;a=RealtimeUpdate._deleteurl.replace("0000000000",c);b='<a class="notice_delete" href="'+a+'" title="Delete this notice">Delete</a>';return b},initActions:function(a,c,d,b,e){$("#notices_primary").prepend('<ul id="realtime_actions"><li id="realtime_playpause"></li><li id="realtime_timeline"></li></ul>');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('<li id="realtime_playpause"><button id="realtime_pause" class="pause"></button></li>');$("#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('<li id="realtime_playpause"><span id="queued_counter"></span> <button id="realtime_play" class="play"></button></li>');$("#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('<button id="realtime_popup"></button>');$("#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