also making the file structure better with js and css folders for Realtime and LinkPreview
* @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
*
{
$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;
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);
}
+++ /dev/null
-<?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);
- }
-}
+++ /dev/null
-// 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(); } );
- }
- }
-}();
--- /dev/null
+<?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);
+ }
+}
+++ /dev/null
-/**
- * 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);
--- /dev/null
+// 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(); } );
+ }
+ }
+}();
--- /dev/null
+/**
+ * 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);
{
$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'),
--- /dev/null
+/**
+ * (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);
+ }
+})();
+++ /dev/null
-/**
- * (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);
- }
-})();
+++ /dev/null
-(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
* @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
*
} 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;
}
--- /dev/null
+// 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();
+ }
+ }
+}();
+
+++ /dev/null
-// 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();
- }
- }
-}();
-
+++ /dev/null
-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
* @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
*
$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;
}
--- /dev/null
+TCPSocket = Orbited.TCPSocket;
+
--- /dev/null
+// 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);
+ }
+ }
+}();
+
+++ /dev/null
-TCPSocket = Orbited.TCPSocket;
-
+++ /dev/null
-// 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);
- }
- }
-}();
-
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;
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'));
}
/**
--- /dev/null
+.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;
+}
--- /dev/null
+/*
+ * 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%'});
+ }
+}
+
+++ /dev/null
-.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;
-}
+++ /dev/null
-/*
- * 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%'});
- }
-}
-
+++ /dev/null
-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