diff --git a/plugins/Realtime/RealtimePlugin.php b/plugins/Realtime/RealtimePlugin.php index 5396d6d38e..eca3a8b029 100644 --- a/plugins/Realtime/RealtimePlugin.php +++ b/plugins/Realtime/RealtimePlugin.php @@ -93,16 +93,24 @@ class RealtimePlugin extends Plugin */ function onRouterInitialized($m) { - // Discovery actions - $m->connect('main/channel/:channel_key/keepalive', - array('action' => 'keepalivechannel')); - $m->connect('main/channel/:channel_key/close', - array('action' => 'closechannel')); + $m->connect('main/channel/:channelkey/keepalive', + array('action' => 'keepalivechannel'), + array('channelkey' => '[a-z0-9]{32}')); + $m->connect('main/channel/:channelkey/close', + array('action' => 'closechannel'), + array('channelkey' => '[a-z0-9]{32}')); + return true; } function onEndShowScripts($action) { - $timeline = $this->_getTimeline($action); + $channel = $this->_getChannel($action); + + if (empty($channel)) { + return true; + } + + $timeline = $this->_pathToChannel(array($channel->channel_key)); // If there's not a timeline on this page, // just return true @@ -137,12 +145,14 @@ class RealtimePlugin extends Plugin } else { $pluginPath = common_path('plugins/Realtime/'); - $realtimeUI = ' RealtimeUpdate.initActions("'.$url.'", "'.$timeline.'", "'. $pluginPath .'");'; + $keepalive = common_local_url('keepalivechannel', array('channelkey' => $channel->channel_key)); + $close = common_local_url('closechannel', array('channelkey' => $channel->channel_key)); + $realtimeUI = ' RealtimeUpdate.initActions("'.$url.'", "'.$timeline.'", "'. $pluginPath .'", "'.$keepalive.'", "'.$close.'"); '; } $script = ' $(document).ready(function() { '. $realtimeUI. - $this->_updateInitialize($timeline, $user_id). + $this->_updateInitialize($timeline, $user_id). '}); '; $action->inlineScript($script); @@ -431,7 +441,18 @@ class RealtimePlugin extends Plugin return ''; } + function _getTimeline($action) + { + $channel = $this->_getChannel($action); + if (empty($channel)) { + return null; + } + + return $this->_pathToChannel(array($channel->channel_key)); + } + + function _getChannel($action) { $timeline = null; $arg1 = null; @@ -480,12 +501,8 @@ class RealtimePlugin extends Plugin $action_name, $arg1, $arg2); - - if (!empty($channel)) { - $timeline = $this->_pathToChannel(array($channel->channel_key)); - } - - return $timeline; + + return $channel; } function onStartReadWriteTables(&$alwaysRW, &$rwdb) diff --git a/plugins/Realtime/Realtime_channel.php b/plugins/Realtime/Realtime_channel.php index 9b38ae9970..679ff1273a 100644 --- a/plugins/Realtime/Realtime_channel.php +++ b/plugins/Realtime/Realtime_channel.php @@ -58,6 +58,7 @@ class Realtime_channel extends Managed_DataObject public $arg1; // argument public $arg2; // argument, usually null public $channel_key; // 128-bit shared secret key + public $audience; // listener count public $created; // created date public $modified; // modified date @@ -117,6 +118,10 @@ class Realtime_channel extends Managed_DataObject 'length' => 32, 'not null' => true, 'description' => 'shared secret key for this channel'), + 'audience' => array('type' => 'integer', + 'not null' => true, + 'default' => 0, + 'description' => 'reference count'), 'created' => array('type' => 'datetime', 'not null' => true, 'description' => 'date this record was created'), @@ -144,6 +149,7 @@ class Realtime_channel extends Managed_DataObject $channel->action = $action; $channel->arg1 = $arg1; $channel->arg2 = $arg2; + $channel->audience = 1; $channel->channel_key = common_good_rand(16); // 128-bit key, 32 hex chars @@ -230,18 +236,40 @@ class Realtime_channel extends Managed_DataObject } if ($channel->find(true)) { - $channel->touch(); + $channel->increment(); return $channel; } else { return null; } } + function increment() + { + // XXX: race + $orig = clone($this); + $this->audience++; + $this->modified = common_sql_now(); + $this->update($orig); + } + function touch() { - // Touch it! + // XXX: race $orig = clone($this); $this->modified = common_sql_now(); $this->update($orig); } -} \ No newline at end of file + + function decrement() + { + // XXX: race + if ($this->audience == 1) { + $this->delete(); + } else { + $orig = clone($this); + $this->audience--; + $this->modified = common_sql_now(); + $this->update($orig); + } + } +} diff --git a/plugins/Realtime/closechannel.php b/plugins/Realtime/closechannel.php index f07e24b297..63c616e5db 100644 --- a/plugins/Realtime/closechannel.php +++ b/plugins/Realtime/closechannel.php @@ -66,7 +66,7 @@ class ClosechannelAction extends Action throw new ClientException(_m('You have to POST it.')); } - $this->channelKey = $this->trimmed('channel_key'); + $this->channelKey = $this->trimmed('channelkey'); if (empty($this->channelKey)) { throw new ClientException(_m('No channel key argument.')); @@ -91,7 +91,7 @@ class ClosechannelAction extends Action function handle($argarray=null) { - $this->channel->delete(); + $this->channel->decrement(); header('HTTP/1.1 204 No Content'); diff --git a/plugins/Realtime/keepalivechannel.php b/plugins/Realtime/keepalivechannel.php index dc1bfb6d24..152595d76c 100644 --- a/plugins/Realtime/keepalivechannel.php +++ b/plugins/Realtime/keepalivechannel.php @@ -66,7 +66,7 @@ class KeepalivechannelAction extends Action throw new ClientException(_m('You have to POST it.')); } - $this->channelKey = $this->trimmed('channel_key'); + $this->channelKey = $this->trimmed('channelkey'); if (empty($this->channelKey)) { throw new ClientException(_m('No channel key argument.')); diff --git a/plugins/Realtime/realtimeupdate.js b/plugins/Realtime/realtimeupdate.js index d9f5e7604f..ab7d529f65 100644 --- a/plugins/Realtime/realtimeupdate.js +++ b/plugins/Realtime/realtimeupdate.js @@ -1,6 +1,6 @@ /* * StatusNet - a distributed open-source microblogging tool - * Copyright (C) 2008, StatusNet, Inc. + * Copyright (C) 2009-2011, StatusNet, Inc. * * Add a notice encoded as JSON into the current timeline * @@ -21,7 +21,7 @@ * @package StatusNet * @author Evan Prodromou * @author Sarven Capadisli - * @copyright 2009 StatusNet, Inc. + * @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/ */ @@ -45,6 +45,8 @@ RealtimeUpdate = { _userid: 0, _showurl: '', + _keepaliveurl: '', + _closeurl: '', _updatecounter: 0, _maxnotices: 50, _windowhasfocus: true, @@ -390,11 +392,28 @@ RealtimeUpdate = { * * @access private */ - initActions: function(url, timeline, path) + initActions: function(url, timeline, path, keepaliveurl, closeurl) { $('#notices_primary').prepend(''); RealtimeUpdate._pluginPath = path; + RealtimeUpdate._keepaliveurl = keepaliveurl; + RealtimeUpdate._closeurl = closeurl; + + + // On unload, let the server know we're no longer listening + $(window).unload(function() { + $.ajax({ + type: 'POST', + url: RealtimeUpdate._closeurl}); + }); + + setInterval(function() { + $.ajax({ + type: 'POST', + url: RealtimeUpdate._keepaliveurl}); + + }, 15 * 60 * 1000 ); // every 15 min; timeout in 30 min RealtimeUpdate.initPlayPause(); RealtimeUpdate.initAddPopup(url, timeline, RealtimeUpdate._pluginPath); diff --git a/plugins/Realtime/realtimeupdate.min.js b/plugins/Realtime/realtimeupdate.min.js index a7453f3a16..ad3fb97a76 100644 --- a/plugins/Realtime/realtimeupdate.min.js +++ b/plugins/Realtime/realtimeupdate.min.js @@ -1 +1 @@ -RealtimeUpdate={_userid:0,_showurl:"",_updatecounter:0,_maxnotices:50,_windowhasfocus:true,_documenttitle:"",_paused:false,_queuedNotices:[],init:function(a,b){RealtimeUpdate._userid=a;RealtimeUpdate._showurl=b;RealtimeUpdate._documenttitle=document.title;$(window).bind("focus",function(){RealtimeUpdate._windowhasfocus=true;RealtimeUpdate._updatecounter=0;RealtimeUpdate.removeWindowCounter()});$(window).bind("blur",function(){$("#notices_primary .notice").removeClass("mark-top");$("#notices_primary .notice:first").addClass("mark-top");RealtimeUpdate._windowhasfocus=false;return false})},receive:function(a){if(RealtimeUpdate.isNoticeVisible(a.id)){return}if(RealtimeUpdate._paused===false){RealtimeUpdate.purgeLastNoticeItem();RealtimeUpdate.insertNoticeItem(a)}else{RealtimeUpdate._queuedNotices.push(a);RealtimeUpdate.updateQueuedCounter()}RealtimeUpdate.updateWindowCounter()},insertNoticeItem:function(a){if(RealtimeUpdate.isNoticeVisible(a.id)){return}RealtimeUpdate.makeNoticeItem(a,function(b){if(RealtimeUpdate.isNoticeVisible(a.id)){return}var c=$(b).attr("id");var d=$("#notices_primary .notices:first");var j=true;var e=d.hasClass("threaded-notices");if(e&&a.in_reply_to_status_id){var g=$("#notice-"+a.in_reply_to_status_id);if(g.length==0){}else{var h=g.closest(".notices");if(h.hasClass("threaded-replies")){g=h.closest(".notice")}d=g.find(".threaded-replies");if(d.length==0){d=$('');g.append(d);SN.U.NoticeInlineReplyPlaceholder(g)}j=false}}var i=$(b);if(j){d.prepend(i)}else{var f=d.find("li.notice-reply-placeholder");if(f.length>0){i.insertBefore(f)}else{i.appendTo(d)}}i.css({display:"none"}).fadeIn(1000);SN.U.NoticeReplyTo($("#"+c));SN.U.NoticeWithAttachment($("#"+c))})},isNoticeVisible:function(a){return($("#notice-"+a).length>0)},purgeLastNoticeItem:function(){if($("#notices_primary .notice").length>RealtimeUpdate._maxnotices){$("#notices_primary .notice:last").remove()}},updateWindowCounter:function(){if(RealtimeUpdate._windowhasfocus===false){RealtimeUpdate._updatecounter+=1;document.title="("+RealtimeUpdate._updatecounter+") "+RealtimeUpdate._documenttitle}},removeWindowCounter:function(){document.title=RealtimeUpdate._documenttitle},makeNoticeItem:function(b,c){var a=RealtimeUpdate._showurl.replace("0000000000",b.id);$.get(a,{ajax:1},function(f,h,g){var e=$("li.notice:first",f);if(e.length){var d=document._importNode(e[0],true);c(d)}})},makeFavoriteForm:function(c,b){var a;a='
Favor this notice
';return a},makeReplyLink:function(c,a){var b;b='Reply '+c+"";return b},makeRepeatForm:function(c,b){var a;a='
Repeat this notice?
';return a},makeDeleteLink:function(c){var b,a;a=RealtimeUpdate._deleteurl.replace("0000000000",c);b='Delete';return b},initActions:function(a,b,c){$("#notices_primary").prepend('');RealtimeUpdate._pluginPath=c;RealtimeUpdate.initPlayPause();RealtimeUpdate.initAddPopup(a,b,RealtimeUpdate._pluginPath)},initPlayPause:function(){if(typeof(localStorage)=="undefined"){RealtimeUpdate.showPause()}else{if(localStorage.getItem("RealtimeUpdate_paused")==="true"){RealtimeUpdate.showPlay()}else{RealtimeUpdate.showPause()}}},showPause:function(){RealtimeUpdate.setPause(false);RealtimeUpdate.showQueuedNotices();RealtimeUpdate.addNoticesHover();$("#realtime_playpause").remove();$("#realtime_actions").prepend('
  • ');$("#realtime_pause").text(SN.msg("realtime_pause")).attr("title",SN.msg("realtime_pause_tooltip")).bind("click",function(){RealtimeUpdate.removeNoticesHover();RealtimeUpdate.showPlay();return false})},showPlay:function(){RealtimeUpdate.setPause(true);$("#realtime_playpause").remove();$("#realtime_actions").prepend('
  • ');$("#realtime_play").text(SN.msg("realtime_play")).attr("title",SN.msg("realtime_play_tooltip")).bind("click",function(){RealtimeUpdate.showPause();return false})},setPause:function(a){RealtimeUpdate._paused=a;if(typeof(localStorage)!="undefined"){localStorage.setItem("RealtimeUpdate_paused",RealtimeUpdate._paused)}},showQueuedNotices:function(){$.each(RealtimeUpdate._queuedNotices,function(a,b){RealtimeUpdate.insertNoticeItem(b)});RealtimeUpdate._queuedNotices=[];RealtimeUpdate.removeQueuedCounter()},updateQueuedCounter:function(){$("#realtime_playpause #queued_counter").html("("+RealtimeUpdate._queuedNotices.length+")")},removeQueuedCounter:function(){$("#realtime_playpause #queued_counter").empty()},addNoticesHover:function(){$("#notices_primary .notices").hover(function(){if(RealtimeUpdate._paused===false){RealtimeUpdate.showPlay()}},function(){if(RealtimeUpdate._paused===true){RealtimeUpdate.showPause()}})},removeNoticesHover:function(){$("#notices_primary .notices").unbind()},initAddPopup:function(a,b,c){$("#realtime_timeline").append('');$("#realtime_popup").text(SN.msg("realtime_popup")).attr("title",SN.msg("realtime_popup_tooltip")).bind("click",function(){window.open(a,"","toolbar=no,resizable=yes,scrollbars=yes,status=no,menubar=no,personalbar=no,location=no,width=500,height=550");return false})},initPopupWindow:function(){$(".notices .entry-title a, .notices .entry-content a").bind("click",function(){window.open(this.href,"");return false});$("#showstream .entity_profile").css({width:"69%"})}}; \ No newline at end of file +RealtimeUpdate={_userid:0,_showurl:"",_keepaliveurl:"",_closeurl:"",_updatecounter:0,_maxnotices:50,_windowhasfocus:true,_documenttitle:"",_paused:false,_queuedNotices:[],init:function(a,b){RealtimeUpdate._userid=a;RealtimeUpdate._showurl=b;RealtimeUpdate._documenttitle=document.title;$(window).bind("focus",function(){RealtimeUpdate._windowhasfocus=true;RealtimeUpdate._updatecounter=0;RealtimeUpdate.removeWindowCounter()});$(window).bind("blur",function(){$("#notices_primary .notice").removeClass("mark-top");$("#notices_primary .notice:first").addClass("mark-top");RealtimeUpdate._windowhasfocus=false;return false})},receive:function(a){if(RealtimeUpdate.isNoticeVisible(a.id)){return}if(RealtimeUpdate._paused===false){RealtimeUpdate.purgeLastNoticeItem();RealtimeUpdate.insertNoticeItem(a)}else{RealtimeUpdate._queuedNotices.push(a);RealtimeUpdate.updateQueuedCounter()}RealtimeUpdate.updateWindowCounter()},insertNoticeItem:function(a){if(RealtimeUpdate.isNoticeVisible(a.id)){return}RealtimeUpdate.makeNoticeItem(a,function(b){if(RealtimeUpdate.isNoticeVisible(a.id)){return}var c=$(b).attr("id");var d=$("#notices_primary .notices:first");var j=true;var e=d.hasClass("threaded-notices");if(e&&a.in_reply_to_status_id){var g=$("#notice-"+a.in_reply_to_status_id);if(g.length==0){}else{var h=g.closest(".notices");if(h.hasClass("threaded-replies")){g=h.closest(".notice")}d=g.find(".threaded-replies");if(d.length==0){d=$('');g.append(d);SN.U.NoticeInlineReplyPlaceholder(g)}j=false}}var i=$(b);if(j){d.prepend(i)}else{var f=d.find("li.notice-reply-placeholder");if(f.length>0){i.insertBefore(f)}else{i.appendTo(d)}}i.css({display:"none"}).fadeIn(1000);SN.U.NoticeReplyTo($("#"+c));SN.U.NoticeWithAttachment($("#"+c))})},isNoticeVisible:function(a){return($("#notice-"+a).length>0)},purgeLastNoticeItem:function(){if($("#notices_primary .notice").length>RealtimeUpdate._maxnotices){$("#notices_primary .notice:last").remove()}},updateWindowCounter:function(){if(RealtimeUpdate._windowhasfocus===false){RealtimeUpdate._updatecounter+=1;document.title="("+RealtimeUpdate._updatecounter+") "+RealtimeUpdate._documenttitle}},removeWindowCounter:function(){document.title=RealtimeUpdate._documenttitle},makeNoticeItem:function(b,c){var a=RealtimeUpdate._showurl.replace("0000000000",b.id);$.get(a,{ajax:1},function(f,h,g){var e=$("li.notice:first",f);if(e.length){var d=document._importNode(e[0],true);c(d)}})},makeFavoriteForm:function(c,b){var a;a='
    Favor this notice
    ';return a},makeReplyLink:function(c,a){var b;b='Reply '+c+"";return b},makeRepeatForm:function(c,b){var a;a='
    Repeat this notice?
    ';return a},makeDeleteLink:function(c){var b,a;a=RealtimeUpdate._deleteurl.replace("0000000000",c);b='Delete';return b},initActions:function(a,c,d,b,e){$("#notices_primary").prepend('');RealtimeUpdate._pluginPath=d;RealtimeUpdate._keepaliveurl=b;RealtimeUpdate._closeurl=e;$(window).unload(function(){$.ajax({type:"POST",url:RealtimeUpdate._closeurl})});setInterval(function(){$.ajax({type:"POST",url:RealtimeUpdate._keepaliveurl})},15*60*1000);RealtimeUpdate.initPlayPause();RealtimeUpdate.initAddPopup(a,c,RealtimeUpdate._pluginPath)},initPlayPause:function(){if(typeof(localStorage)=="undefined"){RealtimeUpdate.showPause()}else{if(localStorage.getItem("RealtimeUpdate_paused")==="true"){RealtimeUpdate.showPlay()}else{RealtimeUpdate.showPause()}}},showPause:function(){RealtimeUpdate.setPause(false);RealtimeUpdate.showQueuedNotices();RealtimeUpdate.addNoticesHover();$("#realtime_playpause").remove();$("#realtime_actions").prepend('
  • ');$("#realtime_pause").text(SN.msg("realtime_pause")).attr("title",SN.msg("realtime_pause_tooltip")).bind("click",function(){RealtimeUpdate.removeNoticesHover();RealtimeUpdate.showPlay();return false})},showPlay:function(){RealtimeUpdate.setPause(true);$("#realtime_playpause").remove();$("#realtime_actions").prepend('
  • ');$("#realtime_play").text(SN.msg("realtime_play")).attr("title",SN.msg("realtime_play_tooltip")).bind("click",function(){RealtimeUpdate.showPause();return false})},setPause:function(a){RealtimeUpdate._paused=a;if(typeof(localStorage)!="undefined"){localStorage.setItem("RealtimeUpdate_paused",RealtimeUpdate._paused)}},showQueuedNotices:function(){$.each(RealtimeUpdate._queuedNotices,function(a,b){RealtimeUpdate.insertNoticeItem(b)});RealtimeUpdate._queuedNotices=[];RealtimeUpdate.removeQueuedCounter()},updateQueuedCounter:function(){$("#realtime_playpause #queued_counter").html("("+RealtimeUpdate._queuedNotices.length+")")},removeQueuedCounter:function(){$("#realtime_playpause #queued_counter").empty()},addNoticesHover:function(){$("#notices_primary .notices").hover(function(){if(RealtimeUpdate._paused===false){RealtimeUpdate.showPlay()}},function(){if(RealtimeUpdate._paused===true){RealtimeUpdate.showPause()}})},removeNoticesHover:function(){$("#notices_primary .notices").unbind()},initAddPopup:function(a,b,c){$("#realtime_timeline").append('');$("#realtime_popup").text(SN.msg("realtime_popup")).attr("title",SN.msg("realtime_popup_tooltip")).bind("click",function(){window.open(a,"","toolbar=no,resizable=yes,scrollbars=yes,status=no,menubar=no,personalbar=no,location=no,width=500,height=550");return false})},initPopupWindow:function(){$(".notices .entry-title a, .notices .entry-content a").bind("click",function(){window.open(this.href,"");return false});$("#showstream .entity_profile").css({width:"69%"})}}; \ No newline at end of file