diff --git a/actions/login.php b/actions/login.php
index 768bc04cef..547374a12e 100644
--- a/actions/login.php
+++ b/actions/login.php
@@ -297,4 +297,8 @@ class LoginAction extends Action
$nav = new LoginGroupNav($this);
$nav->show();
}
+
+ function showNoticeForm()
+ {
+ }
}
diff --git a/actions/recoverpassword.php b/actions/recoverpassword.php
index 9019d6fb22..a73872bfdb 100644
--- a/actions/recoverpassword.php
+++ b/actions/recoverpassword.php
@@ -282,7 +282,11 @@ class RecoverpasswordAction extends Action
$user = User::staticGet('email', common_canonical_email($nore));
if (!$user) {
- $user = User::staticGet('nickname', common_canonical_nickname($nore));
+ try {
+ $user = User::staticGet('nickname', common_canonical_nickname($nore));
+ } catch (NicknameException $e) {
+ // invalid
+ }
}
# See if it's an unconfirmed email address
diff --git a/actions/register.php b/actions/register.php
index 6b039c93f6..d0dbceeb81 100644
--- a/actions/register.php
+++ b/actions/register.php
@@ -606,4 +606,8 @@ class RegisterAction extends Action
$nav = new LoginGroupNav($this);
$nav->show();
}
+
+ function showNoticeForm()
+ {
+ }
}
diff --git a/js/util.js b/js/util.js
index dcb8da4600..cc95a08bf5 100644
--- a/js/util.js
+++ b/js/util.js
@@ -104,7 +104,7 @@ var SN = { // StatusNet
SN.U.Counter(form);
- NDT = form.find('[name=status_textarea]');
+ NDT = form.find('.notice_data-text:first');
NDT.bind('keyup', function(e) {
SN.U.Counter(form);
@@ -183,7 +183,7 @@ var SN = { // StatusNet
* @return number of chars
*/
CharacterCount: function(form) {
- return form.find('[name=status_textarea]').val().length;
+ return form.find('.notice_data-text:first').val().length;
},
/**
@@ -327,7 +327,7 @@ var SN = { // StatusNet
dataType: 'xml',
timeout: '60000',
beforeSend: function(formData) {
- if (form.find('[name=status_textarea]').val() == '') {
+ if (form.find('.notice_data-text:first').val() == '') {
form.addClass(SN.C.S.Warning);
return false;
}
@@ -612,10 +612,7 @@ var SN = { // StatusNet
list.append(replyItem);
var form = replyForm = $(formEl);
- SN.U.NoticeLocationAttach(form);
- SN.U.FormNoticeXHR(form);
- SN.U.FormNoticeEnhancements(form);
- SN.U.NoticeDataAttach(form);
+ SN.Init.NoticeFormSetup(form);
nextStep();
};
@@ -1263,7 +1260,7 @@ var SN = { // StatusNet
var profileLink = $('#nav_profile a').attr('href');
if (profileLink) {
- var authorUrl = $(notice).find('.entry-title .author a.url').attr('href');
+ var authorUrl = $(notice).find('.vcard.author a.url').attr('href');
if (authorUrl == profileLink) {
if (action == 'all' || action == 'showstream') {
// Posts always show on your own friends and profile streams.
@@ -1280,6 +1277,13 @@ var SN = { // StatusNet
return false;
},
+ /**
+ * Switch to another active input sub-form.
+ * This will hide the current form (if any), show the new one, and
+ * update the input type tab selection state.
+ *
+ * @param {String} tag
+ */
switchInputFormTab: function(tag) {
// The one that's current isn't current anymore
$('.input_form_nav_tab.current').removeClass('current');
@@ -1301,16 +1305,27 @@ var SN = { // StatusNet
*/
NoticeForm: function() {
if ($('body.user_in').length > 0) {
- $('.'+SN.C.S.FormNotice).each(function() {
+ $('.ajax-notice').each(function() {
var form = $(this);
- SN.U.NoticeLocationAttach(form);
- SN.U.FormNoticeXHR(form);
- SN.U.FormNoticeEnhancements(form);
- SN.U.NoticeDataAttach(form);
+ SN.Init.NoticeFormSetup(form);
});
}
},
+ /**
+ * Encapsulate notice form setup for a single form.
+ * Plugins can add extra setup by monkeypatching this
+ * function.
+ *
+ * @param {jQuery} form
+ */
+ NoticeFormSetup: function(form) {
+ SN.U.NoticeLocationAttach(form);
+ SN.U.FormNoticeXHR(form);
+ SN.U.FormNoticeEnhancements(form);
+ SN.U.NoticeDataAttach(form);
+ },
+
/**
* Run setup code for notice timeline views items:
*
diff --git a/js/util.min.js b/js/util.min.js
index 5e08cc78b5..32a893355f 100644
--- a/js/util.min.js
+++ b/js/util.min.js
@@ -1 +1 @@
-var SN={C:{I:{CounterBlackout:false,MaxLength:140,PatternUsername:/^[0-9a-zA-Z\-_.]*$/,HTTP20x30x:[200,201,202,203,204,205,206,300,301,302,303,304,305,306,307],NoticeFormMaster:null},S:{Disabled:"disabled",Warning:"warning",Error:"error",Success:"success",Processing:"processing",CommandResult:"command_result",FormNotice:"form_notice",NoticeDataGeo:"notice_data-geo",NoticeDataGeoCookie:"NoticeDataGeo",NoticeDataGeoSelected:"notice_data-geo_selected",StatusNetInstance:"StatusNetInstance"}},messages:{},msg:function(a){if(typeof SN.messages[a]=="undefined"){return"["+a+"]"}else{return SN.messages[a]}},U:{FormNoticeEnhancements:function(b){if(jQuery.data(b[0],"ElementData")===undefined){MaxLength=b.find(".count").text();if(typeof(MaxLength)=="undefined"){MaxLength=SN.C.I.MaxLength}jQuery.data(b[0],"ElementData",{MaxLength:MaxLength});SN.U.Counter(b);NDT=b.find("[name=status_textarea]");NDT.bind("keyup",function(c){SN.U.Counter(b)});var a=function(c){window.setTimeout(function(){SN.U.Counter(b)},50)};NDT.bind("cut",a).bind("paste",a)}else{b.find(".count").text(jQuery.data(b[0],"ElementData").MaxLength)}},Counter:function(d){SN.C.I.FormNoticeCurrent=d;var b=jQuery.data(d[0],"ElementData").MaxLength;if(b<=0){return}var c=b-SN.U.CharacterCount(d);var a=d.find(".count");if(c.toString()!=a.text()){if(!SN.C.I.CounterBlackout||c===0){if(a.text()!=String(c)){a.text(c)}if(c<0){d.addClass(SN.C.S.Warning)}else{d.removeClass(SN.C.S.Warning)}if(!SN.C.I.CounterBlackout){SN.C.I.CounterBlackout=true;SN.C.I.FormNoticeCurrent=d;window.setTimeout("SN.U.ClearCounterBlackout(SN.C.I.FormNoticeCurrent);",500)}}}},CharacterCount:function(a){return a.find("[name=status_textarea]").val().length},ClearCounterBlackout:function(a){SN.C.I.CounterBlackout=false;SN.U.Counter(a)},RewriteAjaxAction:function(a){if(document.location.protocol=="https:"&&a.substr(0,5)=="http:"){return a.replace(/^http:\/\/[^:\/]+/,"https://"+document.location.host)}else{return a}},FormXHR:function(a){$.ajax({type:"POST",dataType:"xml",url:SN.U.RewriteAjaxAction(a.attr("action")),data:a.serialize()+"&ajax=1",beforeSend:function(b){a.addClass(SN.C.S.Processing).find(".submit").addClass(SN.C.S.Disabled).attr(SN.C.S.Disabled,SN.C.S.Disabled)},error:function(c,d,b){alert(b||d)},success:function(b,c){if(typeof($("form",b)[0])!="undefined"){form_new=document._importNode($("form",b)[0],true);a.replaceWith(form_new)}else{a.replaceWith(document._importNode($("p",b)[0],true))}}})},FormNoticeXHR:function(b){SN.C.I.NoticeDataGeo={};b.append(' ');b.attr("action",SN.U.RewriteAjaxAction(b.attr("action")));var c=function(d,e){b.append($('
').addClass(d).text(e))};var a=function(){b.find(".form_response").remove()};b.ajaxForm({dataType:"xml",timeout:"60000",beforeSend:function(d){if(b.find("[name=status_textarea]").val()==""){b.addClass(SN.C.S.Warning);return false}b.addClass(SN.C.S.Processing).find(".submit").addClass(SN.C.S.Disabled).attr(SN.C.S.Disabled,SN.C.S.Disabled);SN.U.normalizeGeoData(b);return true},error:function(f,g,e){b.removeClass(SN.C.S.Processing).find(".submit").removeClass(SN.C.S.Disabled).removeAttr(SN.C.S.Disabled,SN.C.S.Disabled);a();if(g=="timeout"){c("error","Sorry! We had trouble sending your notice. The servers are overloaded. Please try again, and contact the site administrator if this problem persists.")}else{var d=SN.U.GetResponseXML(f);if($("."+SN.C.S.Error,d).length>0){b.append(document._importNode($("."+SN.C.S.Error,d)[0],true))}else{if(parseInt(f.status)===0||jQuery.inArray(parseInt(f.status),SN.C.I.HTTP20x30x)>=0){b.resetForm().find(".attach-status").remove();SN.U.FormNoticeEnhancements(b)}else{c("error","(Sorry! We had trouble sending your notice ("+f.status+" "+f.statusText+"). Please report the problem to the site administrator if this happens again.")}}}},success:function(i,f){a();var n=$("#"+SN.C.S.Error,i);if(n.length>0){c("error",n.text())}else{if($("body")[0].id=="bookmarklet"){self.close()}var d=$("#"+SN.C.S.CommandResult,i);if(d.length>0){c("success",d.text())}else{var m=document._importNode($("li",i)[0],true);var k=$("#notices_primary .notices:first");var l=b.closest("li.notice-reply");if(l.length>0){var e=$(m).attr("id");if($("#"+e).length==0){var j=l.closest("li.notice");l.replaceWith(m);SN.U.NoticeInlineReplyPlaceholder(j)}else{l.remove()}}else{if(k.length>0&&SN.U.belongsOnTimeline(m)){if($("#"+m.id).length===0){var h=b.find("[name=inreplyto]").val();var g="#notices_primary #notice-"+h;if($("body")[0].id=="conversation"){if(h.length>0&&$(g+" .notices").length<1){$(g).append('')}$($(g+" .notices")[0]).append(m)}else{k.prepend(m)}$("#"+m.id).css({display:"none"}).fadeIn(2500);SN.U.NoticeWithAttachment($("#"+m.id));SN.U.NoticeReplyTo($("#"+m.id))}}else{c("success",$("title",i).text())}}}b.resetForm();b.find("[name=inreplyto]").val("");b.find(".attach-status").remove();SN.U.FormNoticeEnhancements(b)}},complete:function(d,e){b.removeClass(SN.C.S.Processing).find(".submit").removeAttr(SN.C.S.Disabled).removeClass(SN.C.S.Disabled);b.find("[name=lat]").val(SN.C.I.NoticeDataGeo.NLat);b.find("[name=lon]").val(SN.C.I.NoticeDataGeo.NLon);b.find("[name=location_ns]").val(SN.C.I.NoticeDataGeo.NLNS);b.find("[name=location_id]").val(SN.C.I.NoticeDataGeo.NLID);b.find("[name=notice_data-geo]").attr("checked",SN.C.I.NoticeDataGeo.NDG)}})},normalizeGeoData:function(a){SN.C.I.NoticeDataGeo.NLat=a.find("[name=lat]").val();SN.C.I.NoticeDataGeo.NLon=a.find("[name=lon]").val();SN.C.I.NoticeDataGeo.NLNS=a.find("[name=location_ns]").val();SN.C.I.NoticeDataGeo.NLID=a.find("[name=location_id]").val();SN.C.I.NoticeDataGeo.NDG=a.find("[name=notice_data-geo]").attr("checked");var b=$.cookie(SN.C.S.NoticeDataGeoCookie);if(b!==null&&b!="disabled"){b=JSON.parse(b);SN.C.I.NoticeDataGeo.NLat=a.find("[name=lat]").val(b.NLat).val();SN.C.I.NoticeDataGeo.NLon=a.find("[name=lon]").val(b.NLon).val();if(b.NLNS){SN.C.I.NoticeDataGeo.NLNS=a.find("[name=location_ns]").val(b.NLNS).val();SN.C.I.NoticeDataGeo.NLID=a.find("[name=location_id]").val(b.NLID).val()}else{a.find("[name=location_ns]").val("");a.find("[name=location_id]").val("")}}if(b=="disabled"){SN.C.I.NoticeDataGeo.NDG=a.find("[name=notice_data-geo]").attr("checked",false).attr("checked")}else{SN.C.I.NoticeDataGeo.NDG=a.find("[name=notice_data-geo]").attr("checked",true).attr("checked")}},GetResponseXML:function(b){try{return b.responseXML}catch(a){return(new DOMParser()).parseFromString(b.responseText,"text/xml")}},NoticeReply:function(){if($("#content .notice_reply").length>0){$("#content .notice").each(function(){SN.U.NoticeReplyTo($(this))})}},NoticeReplyTo:function(a){a.find(".notice_reply").live("click",function(c){c.preventDefault();var b=($(".author .nickname",a).length>0)?$($(".author .nickname",a)[0]):$(".author .nickname.uid");SN.U.NoticeInlineReplyTrigger(a,"@"+b.text());return false})},NoticeInlineReplyTrigger:function(h,i){var b=$($(".notice_id",h)[0]).text();var e=h;var f=h.closest(".notices");if(f.hasClass("threaded-replies")){e=f.closest(".notice")}else{f=$("ul.threaded-replies",h);if(f.length==0){f=$('');h.append(f)}}var j=$(".notice-reply-form",f);var d=function(){j.find("input[name=inreplyto]").val(b);var m=j.find("textarea");if(m.length==0){throw"No textarea"}var l="";if(i){l=i+" "}m.val(l+m.val().replace(RegExp(l,"i"),""));m.data("initialText",$.trim(i+""));m.focus();if(m[0].setSelectionRange){var k=m.val().length;m[0].setSelectionRange(k,k)}};if(j.length>0){d()}else{$("li.notice-reply-placeholder").remove();var g=$("li.notice-reply",f);if(g.length==0){g=$(' ');var c=function(k){var l=document._importNode(k,true);g.append(l);f.append(g);var m=j=$(l);SN.U.NoticeLocationAttach(m);SN.U.FormNoticeXHR(m);SN.U.FormNoticeEnhancements(m);SN.U.NoticeDataAttach(m);d()};if(SN.C.I.NoticeFormMaster){c(SN.C.I.NoticeFormMaster)}else{var a=$("#form_notice").attr("action");$.get(a,{ajax:1},function(k,m,l){c($("form",k)[0])})}}}},NoticeInlineReplyPlaceholder:function(b){var a=b.find("ul.threaded-replies");var c=$(' ');c.click(function(){SN.U.NoticeInlineReplyTrigger(b)});c.find("input").val(SN.msg("reply_placeholder"));a.append(c)},NoticeInlineReplySetup:function(){$(".threaded-replies").each(function(){var b=$(this);var a=b.closest(".notice");SN.U.NoticeInlineReplyPlaceholder(a)})},NoticeRepeat:function(){$(".form_repeat").live("click",function(a){a.preventDefault();SN.U.NoticeRepeatConfirmation($(this));return false})},NoticeRepeatConfirmation:function(a){var c=a.find(".submit");var b=c.clone();b.addClass("submit_dialogbox").removeClass("submit");a.append(b);b.bind("click",function(){SN.U.FormXHR(a);return false});c.hide();a.addClass("dialogbox").append('× ').closest(".notice-options").addClass("opaque");a.find("button.close").click(function(){$(this).remove();a.removeClass("dialogbox").closest(".notice-options").removeClass("opaque");a.find(".submit_dialogbox").remove();a.find(".submit").show();return false})},NoticeAttachments:function(){$(".notice a.attachment").each(function(){SN.U.NoticeWithAttachment($(this).closest(".notice"))})},NoticeWithAttachment:function(b){if(b.find(".attachment").length===0){return}var a=b.find(".attachment.more");if(a.length>0){$(a[0]).click(function(){var c=$(this);c.addClass(SN.C.S.Processing);$.get(c.attr("href")+"/ajax",null,function(d){c.parent(".entry-content").html($(d).find("#attachment_view .entry-content").html())});return false}).attr("title",SN.msg("showmore_tooltip"))}},NoticeDataAttach:function(b){var a=b.find("input[type=file]");a.change(function(f){b.find(".attach-status").remove();var d=$(this).val();if(!d){return false}var c=$('
×
');c.find("code").text(d);c.find("button").click(function(){c.remove();a.val("");return false});b.append(c);if(typeof this.files=="object"){for(var e=0;eg){f=false}if(f){h(c,function(j){var i=$(" ").attr("title",e).attr("alt",e).attr("src",j).attr("style","height: 120px");d.find(".attach-status").append(i)})}else{var b=$("
").text(e);d.find(".attach-status").append(b)}},NoticeLocationAttach:function(a){var e=a.find("[name=lat]");var k=a.find("[name=lon]");var g=a.find("[name=location_ns]").val();var l=a.find("[name=location_id]").val();var b="";var d=a.find("[name=notice_data-geo]");var c=a.find("[name=notice_data-geo]");var j=a.find("label.notice_data-geo");function f(n){j.attr("title",jQuery.trim(j.text())).removeClass("checked");a.find("[name=lat]").val("");a.find("[name=lon]").val("");a.find("[name=location_ns]").val("");a.find("[name=location_id]").val("");a.find("[name=notice_data-geo]").attr("checked",false);$.cookie(SN.C.S.NoticeDataGeoCookie,"disabled",{path:"/"});if(n){a.find(".geo_status_wrapper").removeClass("success").addClass("error");a.find(".geo_status_wrapper .geo_status").text(n)}else{a.find(".geo_status_wrapper").remove()}}function m(n,o){SN.U.NoticeGeoStatus(a,"Looking up place name...");$.getJSON(n,o,function(p){var q,r;if(typeof(p.location_ns)!="undefined"){a.find("[name=location_ns]").val(p.location_ns);q=p.location_ns}if(typeof(p.location_id)!="undefined"){a.find("[name=location_id]").val(p.location_id);r=p.location_id}if(typeof(p.name)=="undefined"){NLN_text=o.lat+";"+o.lon}else{NLN_text=p.name}SN.U.NoticeGeoStatus(a,NLN_text,o.lat,o.lon,p.url);j.attr("title",NoticeDataGeo_text.ShareDisable+" ("+NLN_text+")");a.find("[name=lat]").val(o.lat);a.find("[name=lon]").val(o.lon);a.find("[name=location_ns]").val(q);a.find("[name=location_id]").val(r);a.find("[name=notice_data-geo]").attr("checked",true);var s={NLat:o.lat,NLon:o.lon,NLNS:q,NLID:r,NLN:NLN_text,NLNU:p.url,NDG:true};$.cookie(SN.C.S.NoticeDataGeoCookie,JSON.stringify(s),{path:"/"})})}if(c.length>0){if($.cookie(SN.C.S.NoticeDataGeoCookie)=="disabled"){c.attr("checked",false)}else{c.attr("checked",true)}var h=a.find(".notice_data-geo_wrap");var i=h.attr("title");h.removeAttr("title");j.attr("title",j.text());c.change(function(){if(c.attr("checked")===true||$.cookie(SN.C.S.NoticeDataGeoCookie)===null){j.attr("title",NoticeDataGeo_text.ShareDisable).addClass("checked");if($.cookie(SN.C.S.NoticeDataGeoCookie)===null||$.cookie(SN.C.S.NoticeDataGeoCookie)=="disabled"){if(navigator.geolocation){SN.U.NoticeGeoStatus(a,"Requesting location from browser...");navigator.geolocation.getCurrentPosition(function(p){a.find("[name=lat]").val(p.coords.latitude);a.find("[name=lon]").val(p.coords.longitude);var q={lat:p.coords.latitude,lon:p.coords.longitude,token:$("#token").val()};m(i,q)},function(p){switch(p.code){case p.PERMISSION_DENIED:f("Location permission denied.");break;case p.TIMEOUT:f("Location lookup timeout.");break}},{timeout:10000})}else{if(e.length>0&&k.length>0){var n={lat:e,lon:k,token:$("#token").val()};m(i,n)}else{f();c.remove();j.remove()}}}else{var o=JSON.parse($.cookie(SN.C.S.NoticeDataGeoCookie));a.find("[name=lat]").val(o.NLat);a.find("[name=lon]").val(o.NLon);a.find("[name=location_ns]").val(o.NLNS);a.find("[name=location_id]").val(o.NLID);a.find("[name=notice_data-geo]").attr("checked",o.NDG);SN.U.NoticeGeoStatus(a,o.NLN,o.NLat,o.NLon,o.NLNU);j.attr("title",NoticeDataGeo_text.ShareDisable+" ("+o.NLN+")").addClass("checked")}}else{f()}}).change()}},NoticeGeoStatus:function(e,a,f,g,c){var h=e.find(".geo_status_wrapper");if(h.length==0){h=$('');h.find("button.close").click(function(){e.find("[name=notice_data-geo]").removeAttr("checked").change()});e.append(h)}var b;if(c){b=$(" ").attr("href",c)}else{b=$(" ")}b.text(a);if(f||g){var d=f+";"+g;b.attr("title",d);if(!a){b.text(d)}}h.find(".geo_status").empty().append(b)},NewDirectMessage:function(){NDM=$(".entity_send-a-message a");NDM.attr({href:NDM.attr("href")+"&ajax=1"});NDM.bind("click",function(){var a=$(".entity_send-a-message form");if(a.length===0){$(this).addClass(SN.C.S.Processing);$.get(NDM.attr("href"),null,function(b){$(".entity_send-a-message").append(document._importNode($("form",b)[0],true));a=$(".entity_send-a-message .form_notice");SN.U.FormNoticeXHR(a);SN.U.FormNoticeEnhancements(a);a.append('× ');$(".entity_send-a-message button").click(function(){a.hide();return false});NDM.removeClass(SN.C.S.Processing)})}else{a.show();$(".entity_send-a-message textarea").focus()}return false})},GetFullYear:function(c,d,a){var b=new Date();b.setFullYear(c,d,a);return b},StatusNetInstance:{Set:function(b){var a=SN.U.StatusNetInstance.Get();if(a!==null){b=$.extend(a,b)}$.cookie(SN.C.S.StatusNetInstance,JSON.stringify(b),{path:"/",expires:SN.U.GetFullYear(2029,0,1)})},Get:function(){var a=$.cookie(SN.C.S.StatusNetInstance);if(a!==null){return JSON.parse(a)}return null},Delete:function(){$.cookie(SN.C.S.StatusNetInstance,null)}},belongsOnTimeline:function(b){var a=$("body").attr("id");if(a=="public"){return true}var c=$("#nav_profile a").attr("href");if(c){var d=$(b).find(".entry-title .author a.url").attr("href");if(d==c){if(a=="all"||a=="showstream"){return true}}}return false},switchInputFormTab:function(a){$(".input_form_nav_tab.current").removeClass("current");$("#input_form_nav_"+a).addClass("current");$(".input_form.current").removeClass("current");$("#input_form_"+a).addClass("current")}},Init:{NoticeForm:function(){if($("body.user_in").length>0){$("."+SN.C.S.FormNotice).each(function(){var a=$(this);SN.U.NoticeLocationAttach(a);SN.U.FormNoticeXHR(a);SN.U.FormNoticeEnhancements(a);SN.U.NoticeDataAttach(a)})}},Notices:function(){if($("body.user_in").length>0){var a=$(".form_notice:first");if(a.length>0){SN.C.I.NoticeFormMaster=document._importNode(a[0],true)}SN.U.NoticeRepeat();SN.U.NoticeReply();SN.U.NoticeInlineReplySetup()}SN.U.NoticeAttachments()},EntityActions:function(){if($("body.user_in").length>0){SN.U.NewDirectMessage()}},Login:function(){if(SN.U.StatusNetInstance.Get()!==null){var a=SN.U.StatusNetInstance.Get().Nickname;if(a!==null){$("#form_login #nickname").val(a)}}$("#form_login").bind("submit",function(){SN.U.StatusNetInstance.Set({Nickname:$("#form_login #nickname").val()});return true})},AjaxForms:function(){$("form.ajax").live("submit",function(){SN.U.FormXHR($(this));return false})},UploadForms:function(){$("input[type=file]").change(function(d){if(typeof this.files=="object"&&this.files.length>0){var c=0;for(var b=0;b0&&c>a){var e="File too large: maximum upload size is %d bytes.";alert(e.replace("%d",a));$(this).val("");d.preventDefault();return false}}})}}};$(document).ready(function(){SN.Init.AjaxForms();SN.Init.UploadForms();if($("."+SN.C.S.FormNotice).length>0){SN.Init.NoticeForm()}if($("#content .notices").length>0){SN.Init.Notices()}if($("#content .entity_actions").length>0){SN.Init.EntityActions()}if($("#form_login").length>0){SN.Init.Login()}});if(!document.ELEMENT_NODE){document.ELEMENT_NODE=1;document.ATTRIBUTE_NODE=2;document.TEXT_NODE=3;document.CDATA_SECTION_NODE=4;document.ENTITY_REFERENCE_NODE=5;document.ENTITY_NODE=6;document.PROCESSING_INSTRUCTION_NODE=7;document.COMMENT_NODE=8;document.DOCUMENT_NODE=9;document.DOCUMENT_TYPE_NODE=10;document.DOCUMENT_FRAGMENT_NODE=11;document.NOTATION_NODE=12}document._importNode=function(e,a){switch(e.nodeType){case document.ELEMENT_NODE:var d=document.createElement(e.nodeName);if(e.attributes&&e.attributes.length>0){for(var c=0,b=e.attributes.length;c0){for(var c=0,b=e.childNodes.length;c0){var j=c.pop();j()}}};window._google_loader_apiLoaded=function(){f()};var d=function(){return(window.google&&google.loader)};var g=function(j){if(d()){return true}h(j);e();return false};e();return{shim:true,type:"ClientLocation",lastPosition:null,getCurrentPosition:function(k,n,o){var m=this;if(!g(function(){m.getCurrentPosition(k,n,o)})){return}if(google.loader.ClientLocation){var l=google.loader.ClientLocation;var j={coords:{latitude:l.latitude,longitude:l.longitude,altitude:null,accuracy:43000,altitudeAccuracy:null,heading:null,speed:null},address:{city:l.address.city,country:l.address.country,country_code:l.address.country_code,region:l.address.region},timestamp:new Date()};k(j);this.lastPosition=j}else{if(n==="function"){n({code:3,message:"Using the Google ClientLocation API and it is not able to calculate a location."})}}},watchPosition:function(j,l,m){this.getCurrentPosition(j,l,m);var k=this;var n=setInterval(function(){k.getCurrentPosition(j,l,m)},10000);return n},clearWatch:function(j){clearInterval(j)},getPermission:function(l,j,k){return true}}});navigator.geolocation=(window.google&&google.gears)?a():b()})()};
\ No newline at end of file
+var SN={C:{I:{CounterBlackout:false,MaxLength:140,PatternUsername:/^[0-9a-zA-Z\-_.]*$/,HTTP20x30x:[200,201,202,203,204,205,206,300,301,302,303,304,305,306,307],NoticeFormMaster:null},S:{Disabled:"disabled",Warning:"warning",Error:"error",Success:"success",Processing:"processing",CommandResult:"command_result",FormNotice:"form_notice",NoticeDataGeo:"notice_data-geo",NoticeDataGeoCookie:"NoticeDataGeo",NoticeDataGeoSelected:"notice_data-geo_selected",StatusNetInstance:"StatusNetInstance"}},messages:{},msg:function(a){if(typeof SN.messages[a]=="undefined"){return"["+a+"]"}else{return SN.messages[a]}},U:{FormNoticeEnhancements:function(b){if(jQuery.data(b[0],"ElementData")===undefined){MaxLength=b.find(".count").text();if(typeof(MaxLength)=="undefined"){MaxLength=SN.C.I.MaxLength}jQuery.data(b[0],"ElementData",{MaxLength:MaxLength});SN.U.Counter(b);NDT=b.find(".notice_data-text:first");NDT.bind("keyup",function(c){SN.U.Counter(b)});var a=function(c){window.setTimeout(function(){SN.U.Counter(b)},50)};NDT.bind("cut",a).bind("paste",a)}else{b.find(".count").text(jQuery.data(b[0],"ElementData").MaxLength)}},Counter:function(d){SN.C.I.FormNoticeCurrent=d;var b=jQuery.data(d[0],"ElementData").MaxLength;if(b<=0){return}var c=b-SN.U.CharacterCount(d);var a=d.find(".count");if(c.toString()!=a.text()){if(!SN.C.I.CounterBlackout||c===0){if(a.text()!=String(c)){a.text(c)}if(c<0){d.addClass(SN.C.S.Warning)}else{d.removeClass(SN.C.S.Warning)}if(!SN.C.I.CounterBlackout){SN.C.I.CounterBlackout=true;SN.C.I.FormNoticeCurrent=d;window.setTimeout("SN.U.ClearCounterBlackout(SN.C.I.FormNoticeCurrent);",500)}}}},CharacterCount:function(a){return a.find(".notice_data-text:first").val().length},ClearCounterBlackout:function(a){SN.C.I.CounterBlackout=false;SN.U.Counter(a)},RewriteAjaxAction:function(a){if(document.location.protocol=="https:"&&a.substr(0,5)=="http:"){return a.replace(/^http:\/\/[^:\/]+/,"https://"+document.location.host)}else{return a}},FormXHR:function(a){$.ajax({type:"POST",dataType:"xml",url:SN.U.RewriteAjaxAction(a.attr("action")),data:a.serialize()+"&ajax=1",beforeSend:function(b){a.addClass(SN.C.S.Processing).find(".submit").addClass(SN.C.S.Disabled).attr(SN.C.S.Disabled,SN.C.S.Disabled)},error:function(c,d,b){alert(b||d)},success:function(b,c){if(typeof($("form",b)[0])!="undefined"){form_new=document._importNode($("form",b)[0],true);a.replaceWith(form_new)}else{a.replaceWith(document._importNode($("p",b)[0],true))}}})},FormNoticeXHR:function(b){SN.C.I.NoticeDataGeo={};b.append(' ');b.attr("action",SN.U.RewriteAjaxAction(b.attr("action")));var c=function(d,e){b.append($('
').addClass(d).text(e))};var a=function(){b.find(".form_response").remove()};b.ajaxForm({dataType:"xml",timeout:"60000",beforeSend:function(d){if(b.find(".notice_data-text:first").val()==""){b.addClass(SN.C.S.Warning);return false}b.addClass(SN.C.S.Processing).find(".submit").addClass(SN.C.S.Disabled).attr(SN.C.S.Disabled,SN.C.S.Disabled);SN.U.normalizeGeoData(b);return true},error:function(f,g,e){b.removeClass(SN.C.S.Processing).find(".submit").removeClass(SN.C.S.Disabled).removeAttr(SN.C.S.Disabled,SN.C.S.Disabled);a();if(g=="timeout"){c("error","Sorry! We had trouble sending your notice. The servers are overloaded. Please try again, and contact the site administrator if this problem persists.")}else{var d=SN.U.GetResponseXML(f);if($("."+SN.C.S.Error,d).length>0){b.append(document._importNode($("."+SN.C.S.Error,d)[0],true))}else{if(parseInt(f.status)===0||jQuery.inArray(parseInt(f.status),SN.C.I.HTTP20x30x)>=0){b.resetForm().find(".attach-status").remove();SN.U.FormNoticeEnhancements(b)}else{c("error","(Sorry! We had trouble sending your notice ("+f.status+" "+f.statusText+"). Please report the problem to the site administrator if this happens again.")}}}},success:function(i,f){a();var n=$("#"+SN.C.S.Error,i);if(n.length>0){c("error",n.text())}else{if($("body")[0].id=="bookmarklet"){self.close()}var d=$("#"+SN.C.S.CommandResult,i);if(d.length>0){c("success",d.text())}else{var m=document._importNode($("li",i)[0],true);var k=$("#notices_primary .notices:first");var l=b.closest("li.notice-reply");if(l.length>0){var e=$(m).attr("id");if($("#"+e).length==0){var j=l.closest("li.notice");l.replaceWith(m);SN.U.NoticeInlineReplyPlaceholder(j)}else{l.remove()}}else{if(k.length>0&&SN.U.belongsOnTimeline(m)){if($("#"+m.id).length===0){var h=b.find("[name=inreplyto]").val();var g="#notices_primary #notice-"+h;if($("body")[0].id=="conversation"){if(h.length>0&&$(g+" .notices").length<1){$(g).append('')}$($(g+" .notices")[0]).append(m)}else{k.prepend(m)}$("#"+m.id).css({display:"none"}).fadeIn(2500);SN.U.NoticeWithAttachment($("#"+m.id));SN.U.NoticeReplyTo($("#"+m.id))}}else{c("success",$("title",i).text())}}}b.resetForm();b.find("[name=inreplyto]").val("");b.find(".attach-status").remove();SN.U.FormNoticeEnhancements(b)}},complete:function(d,e){b.removeClass(SN.C.S.Processing).find(".submit").removeAttr(SN.C.S.Disabled).removeClass(SN.C.S.Disabled);b.find("[name=lat]").val(SN.C.I.NoticeDataGeo.NLat);b.find("[name=lon]").val(SN.C.I.NoticeDataGeo.NLon);b.find("[name=location_ns]").val(SN.C.I.NoticeDataGeo.NLNS);b.find("[name=location_id]").val(SN.C.I.NoticeDataGeo.NLID);b.find("[name=notice_data-geo]").attr("checked",SN.C.I.NoticeDataGeo.NDG)}})},normalizeGeoData:function(a){SN.C.I.NoticeDataGeo.NLat=a.find("[name=lat]").val();SN.C.I.NoticeDataGeo.NLon=a.find("[name=lon]").val();SN.C.I.NoticeDataGeo.NLNS=a.find("[name=location_ns]").val();SN.C.I.NoticeDataGeo.NLID=a.find("[name=location_id]").val();SN.C.I.NoticeDataGeo.NDG=a.find("[name=notice_data-geo]").attr("checked");var b=$.cookie(SN.C.S.NoticeDataGeoCookie);if(b!==null&&b!="disabled"){b=JSON.parse(b);SN.C.I.NoticeDataGeo.NLat=a.find("[name=lat]").val(b.NLat).val();SN.C.I.NoticeDataGeo.NLon=a.find("[name=lon]").val(b.NLon).val();if(b.NLNS){SN.C.I.NoticeDataGeo.NLNS=a.find("[name=location_ns]").val(b.NLNS).val();SN.C.I.NoticeDataGeo.NLID=a.find("[name=location_id]").val(b.NLID).val()}else{a.find("[name=location_ns]").val("");a.find("[name=location_id]").val("")}}if(b=="disabled"){SN.C.I.NoticeDataGeo.NDG=a.find("[name=notice_data-geo]").attr("checked",false).attr("checked")}else{SN.C.I.NoticeDataGeo.NDG=a.find("[name=notice_data-geo]").attr("checked",true).attr("checked")}},GetResponseXML:function(b){try{return b.responseXML}catch(a){return(new DOMParser()).parseFromString(b.responseText,"text/xml")}},NoticeReply:function(){if($("#content .notice_reply").length>0){$("#content .notice").each(function(){SN.U.NoticeReplyTo($(this))})}},NoticeReplyTo:function(a){a.find(".notice_reply").live("click",function(c){c.preventDefault();var b=($(".author .nickname",a).length>0)?$($(".author .nickname",a)[0]):$(".author .nickname.uid");SN.U.NoticeInlineReplyTrigger(a,"@"+b.text());return false})},NoticeInlineReplyTrigger:function(h,i){var b=$($(".notice_id",h)[0]).text();var e=h;var f=h.closest(".notices");if(f.hasClass("threaded-replies")){e=f.closest(".notice")}else{f=$("ul.threaded-replies",h);if(f.length==0){f=$('');h.append(f)}}var j=$(".notice-reply-form",f);var d=function(){j.find("input[name=inreplyto]").val(b);var m=j.find("textarea");if(m.length==0){throw"No textarea"}var l="";if(i){l=i+" "}m.val(l+m.val().replace(RegExp(l,"i"),""));m.data("initialText",$.trim(i+""));m.focus();if(m[0].setSelectionRange){var k=m.val().length;m[0].setSelectionRange(k,k)}};if(j.length>0){d()}else{$("li.notice-reply-placeholder").remove();var g=$("li.notice-reply",f);if(g.length==0){g=$(' ');var c=function(k){var l=document._importNode(k,true);g.append(l);f.append(g);var m=j=$(l);SN.Init.NoticeFormSetup(m);d()};if(SN.C.I.NoticeFormMaster){c(SN.C.I.NoticeFormMaster)}else{var a=$("#form_notice").attr("action");$.get(a,{ajax:1},function(k,m,l){c($("form",k)[0])})}}}},NoticeInlineReplyPlaceholder:function(b){var a=b.find("ul.threaded-replies");var c=$(' ');c.click(function(){SN.U.NoticeInlineReplyTrigger(b)});c.find("input").val(SN.msg("reply_placeholder"));a.append(c)},NoticeInlineReplySetup:function(){$(".threaded-replies").each(function(){var b=$(this);var a=b.closest(".notice");SN.U.NoticeInlineReplyPlaceholder(a)})},NoticeRepeat:function(){$(".form_repeat").live("click",function(a){a.preventDefault();SN.U.NoticeRepeatConfirmation($(this));return false})},NoticeRepeatConfirmation:function(a){var c=a.find(".submit");var b=c.clone();b.addClass("submit_dialogbox").removeClass("submit");a.append(b);b.bind("click",function(){SN.U.FormXHR(a);return false});c.hide();a.addClass("dialogbox").append('× ').closest(".notice-options").addClass("opaque");a.find("button.close").click(function(){$(this).remove();a.removeClass("dialogbox").closest(".notice-options").removeClass("opaque");a.find(".submit_dialogbox").remove();a.find(".submit").show();return false})},NoticeAttachments:function(){$(".notice a.attachment").each(function(){SN.U.NoticeWithAttachment($(this).closest(".notice"))})},NoticeWithAttachment:function(b){if(b.find(".attachment").length===0){return}var a=b.find(".attachment.more");if(a.length>0){$(a[0]).click(function(){var c=$(this);c.addClass(SN.C.S.Processing);$.get(c.attr("href")+"/ajax",null,function(d){c.parent(".entry-content").html($(d).find("#attachment_view .entry-content").html())});return false}).attr("title",SN.msg("showmore_tooltip"))}},NoticeDataAttach:function(b){var a=b.find("input[type=file]");a.change(function(f){b.find(".attach-status").remove();var d=$(this).val();if(!d){return false}var c=$('
×
');c.find("code").text(d);c.find("button").click(function(){c.remove();a.val("");return false});b.append(c);if(typeof this.files=="object"){for(var e=0;eg){f=false}if(f){h(c,function(j){var i=$(" ").attr("title",e).attr("alt",e).attr("src",j).attr("style","height: 120px");d.find(".attach-status").append(i)})}else{var b=$("
").text(e);d.find(".attach-status").append(b)}},NoticeLocationAttach:function(a){var e=a.find("[name=lat]");var k=a.find("[name=lon]");var g=a.find("[name=location_ns]").val();var l=a.find("[name=location_id]").val();var b="";var d=a.find("[name=notice_data-geo]");var c=a.find("[name=notice_data-geo]");var j=a.find("label.notice_data-geo");function f(n){j.attr("title",jQuery.trim(j.text())).removeClass("checked");a.find("[name=lat]").val("");a.find("[name=lon]").val("");a.find("[name=location_ns]").val("");a.find("[name=location_id]").val("");a.find("[name=notice_data-geo]").attr("checked",false);$.cookie(SN.C.S.NoticeDataGeoCookie,"disabled",{path:"/"});if(n){a.find(".geo_status_wrapper").removeClass("success").addClass("error");a.find(".geo_status_wrapper .geo_status").text(n)}else{a.find(".geo_status_wrapper").remove()}}function m(n,o){SN.U.NoticeGeoStatus(a,"Looking up place name...");$.getJSON(n,o,function(p){var q,r;if(typeof(p.location_ns)!="undefined"){a.find("[name=location_ns]").val(p.location_ns);q=p.location_ns}if(typeof(p.location_id)!="undefined"){a.find("[name=location_id]").val(p.location_id);r=p.location_id}if(typeof(p.name)=="undefined"){NLN_text=o.lat+";"+o.lon}else{NLN_text=p.name}SN.U.NoticeGeoStatus(a,NLN_text,o.lat,o.lon,p.url);j.attr("title",NoticeDataGeo_text.ShareDisable+" ("+NLN_text+")");a.find("[name=lat]").val(o.lat);a.find("[name=lon]").val(o.lon);a.find("[name=location_ns]").val(q);a.find("[name=location_id]").val(r);a.find("[name=notice_data-geo]").attr("checked",true);var s={NLat:o.lat,NLon:o.lon,NLNS:q,NLID:r,NLN:NLN_text,NLNU:p.url,NDG:true};$.cookie(SN.C.S.NoticeDataGeoCookie,JSON.stringify(s),{path:"/"})})}if(c.length>0){if($.cookie(SN.C.S.NoticeDataGeoCookie)=="disabled"){c.attr("checked",false)}else{c.attr("checked",true)}var h=a.find(".notice_data-geo_wrap");var i=h.attr("title");h.removeAttr("title");j.attr("title",j.text());c.change(function(){if(c.attr("checked")===true||$.cookie(SN.C.S.NoticeDataGeoCookie)===null){j.attr("title",NoticeDataGeo_text.ShareDisable).addClass("checked");if($.cookie(SN.C.S.NoticeDataGeoCookie)===null||$.cookie(SN.C.S.NoticeDataGeoCookie)=="disabled"){if(navigator.geolocation){SN.U.NoticeGeoStatus(a,"Requesting location from browser...");navigator.geolocation.getCurrentPosition(function(p){a.find("[name=lat]").val(p.coords.latitude);a.find("[name=lon]").val(p.coords.longitude);var q={lat:p.coords.latitude,lon:p.coords.longitude,token:$("#token").val()};m(i,q)},function(p){switch(p.code){case p.PERMISSION_DENIED:f("Location permission denied.");break;case p.TIMEOUT:f("Location lookup timeout.");break}},{timeout:10000})}else{if(e.length>0&&k.length>0){var n={lat:e,lon:k,token:$("#token").val()};m(i,n)}else{f();c.remove();j.remove()}}}else{var o=JSON.parse($.cookie(SN.C.S.NoticeDataGeoCookie));a.find("[name=lat]").val(o.NLat);a.find("[name=lon]").val(o.NLon);a.find("[name=location_ns]").val(o.NLNS);a.find("[name=location_id]").val(o.NLID);a.find("[name=notice_data-geo]").attr("checked",o.NDG);SN.U.NoticeGeoStatus(a,o.NLN,o.NLat,o.NLon,o.NLNU);j.attr("title",NoticeDataGeo_text.ShareDisable+" ("+o.NLN+")").addClass("checked")}}else{f()}}).change()}},NoticeGeoStatus:function(e,a,f,g,c){var h=e.find(".geo_status_wrapper");if(h.length==0){h=$('');h.find("button.close").click(function(){e.find("[name=notice_data-geo]").removeAttr("checked").change()});e.append(h)}var b;if(c){b=$(" ").attr("href",c)}else{b=$(" ")}b.text(a);if(f||g){var d=f+";"+g;b.attr("title",d);if(!a){b.text(d)}}h.find(".geo_status").empty().append(b)},NewDirectMessage:function(){NDM=$(".entity_send-a-message a");NDM.attr({href:NDM.attr("href")+"&ajax=1"});NDM.bind("click",function(){var a=$(".entity_send-a-message form");if(a.length===0){$(this).addClass(SN.C.S.Processing);$.get(NDM.attr("href"),null,function(b){$(".entity_send-a-message").append(document._importNode($("form",b)[0],true));a=$(".entity_send-a-message .form_notice");SN.U.FormNoticeXHR(a);SN.U.FormNoticeEnhancements(a);a.append('× ');$(".entity_send-a-message button").click(function(){a.hide();return false});NDM.removeClass(SN.C.S.Processing)})}else{a.show();$(".entity_send-a-message textarea").focus()}return false})},GetFullYear:function(c,d,a){var b=new Date();b.setFullYear(c,d,a);return b},StatusNetInstance:{Set:function(b){var a=SN.U.StatusNetInstance.Get();if(a!==null){b=$.extend(a,b)}$.cookie(SN.C.S.StatusNetInstance,JSON.stringify(b),{path:"/",expires:SN.U.GetFullYear(2029,0,1)})},Get:function(){var a=$.cookie(SN.C.S.StatusNetInstance);if(a!==null){return JSON.parse(a)}return null},Delete:function(){$.cookie(SN.C.S.StatusNetInstance,null)}},belongsOnTimeline:function(b){var a=$("body").attr("id");if(a=="public"){return true}var c=$("#nav_profile a").attr("href");if(c){var d=$(b).find(".vcard.author a.url").attr("href");if(d==c){if(a=="all"||a=="showstream"){return true}}}return false},switchInputFormTab:function(a){$(".input_form_nav_tab.current").removeClass("current");$("#input_form_nav_"+a).addClass("current");$(".input_form.current").removeClass("current");$("#input_form_"+a).addClass("current")}},Init:{NoticeForm:function(){if($("body.user_in").length>0){$(".ajax-notice").each(function(){var a=$(this);SN.Init.NoticeFormSetup(a)})}},NoticeFormSetup:function(a){SN.U.NoticeLocationAttach(a);SN.U.FormNoticeXHR(a);SN.U.FormNoticeEnhancements(a);SN.U.NoticeDataAttach(a)},Notices:function(){if($("body.user_in").length>0){var a=$(".form_notice:first");if(a.length>0){SN.C.I.NoticeFormMaster=document._importNode(a[0],true)}SN.U.NoticeRepeat();SN.U.NoticeReply();SN.U.NoticeInlineReplySetup()}SN.U.NoticeAttachments()},EntityActions:function(){if($("body.user_in").length>0){SN.U.NewDirectMessage()}},Login:function(){if(SN.U.StatusNetInstance.Get()!==null){var a=SN.U.StatusNetInstance.Get().Nickname;if(a!==null){$("#form_login #nickname").val(a)}}$("#form_login").bind("submit",function(){SN.U.StatusNetInstance.Set({Nickname:$("#form_login #nickname").val()});return true})},AjaxForms:function(){$("form.ajax").live("submit",function(){SN.U.FormXHR($(this));return false})},UploadForms:function(){$("input[type=file]").change(function(d){if(typeof this.files=="object"&&this.files.length>0){var c=0;for(var b=0;b0&&c>a){var e="File too large: maximum upload size is %d bytes.";alert(e.replace("%d",a));$(this).val("");d.preventDefault();return false}}})}}};$(document).ready(function(){SN.Init.AjaxForms();SN.Init.UploadForms();if($("."+SN.C.S.FormNotice).length>0){SN.Init.NoticeForm()}if($("#content .notices").length>0){SN.Init.Notices()}if($("#content .entity_actions").length>0){SN.Init.EntityActions()}if($("#form_login").length>0){SN.Init.Login()}});if(!document.ELEMENT_NODE){document.ELEMENT_NODE=1;document.ATTRIBUTE_NODE=2;document.TEXT_NODE=3;document.CDATA_SECTION_NODE=4;document.ENTITY_REFERENCE_NODE=5;document.ENTITY_NODE=6;document.PROCESSING_INSTRUCTION_NODE=7;document.COMMENT_NODE=8;document.DOCUMENT_NODE=9;document.DOCUMENT_TYPE_NODE=10;document.DOCUMENT_FRAGMENT_NODE=11;document.NOTATION_NODE=12}document._importNode=function(e,a){switch(e.nodeType){case document.ELEMENT_NODE:var d=document.createElement(e.nodeName);if(e.attributes&&e.attributes.length>0){for(var c=0,b=e.attributes.length;c0){for(var c=0,b=e.childNodes.length;c0){var j=c.pop();j()}}};window._google_loader_apiLoaded=function(){f()};var d=function(){return(window.google&&google.loader)};var g=function(j){if(d()){return true}h(j);e();return false};e();return{shim:true,type:"ClientLocation",lastPosition:null,getCurrentPosition:function(k,n,o){var m=this;if(!g(function(){m.getCurrentPosition(k,n,o)})){return}if(google.loader.ClientLocation){var l=google.loader.ClientLocation;var j={coords:{latitude:l.latitude,longitude:l.longitude,altitude:null,accuracy:43000,altitudeAccuracy:null,heading:null,speed:null},address:{city:l.address.city,country:l.address.country,country_code:l.address.country_code,region:l.address.region},timestamp:new Date()};k(j);this.lastPosition=j}else{if(n==="function"){n({code:3,message:"Using the Google ClientLocation API and it is not able to calculate a location."})}}},watchPosition:function(j,l,m){this.getCurrentPosition(j,l,m);var k=this;var n=setInterval(function(){k.getCurrentPosition(j,l,m)},10000);return n},clearWatch:function(j){clearInterval(j)},getPermission:function(l,j,k){return true}}});navigator.geolocation=(window.google&&google.gears)?a():b()})()};
\ No newline at end of file
diff --git a/lib/action.php b/lib/action.php
index 28b0fdbacf..0ba4b8b8ff 100644
--- a/lib/action.php
+++ b/lib/action.php
@@ -267,9 +267,16 @@ class Action extends HTMLOutputter // lawsuit
function primaryCssLink($mainTheme=null, $media=null)
{
+ $theme = new Theme($mainTheme);
+
+ // Some themes may have external stylesheets, such as using the
+ // Google Font APIs to load webfonts.
+ foreach ($theme->getExternals() as $url) {
+ $this->cssLink($url, $mainTheme, $media);
+ }
+
// If the currently-selected theme has dependencies on other themes,
// we'll need to load their display.css files as well in order.
- $theme = new Theme($mainTheme);
$baseThemes = $theme->getDeps();
foreach ($baseThemes as $baseTheme) {
$this->cssLink('css/display.css', $baseTheme, $media);
@@ -595,7 +602,7 @@ class Action extends HTMLOutputter // lawsuit
'class' => 'input_form_nav_tab');
if ($tag == 'status') {
- $attrs['class'] = 'current';
+ $attrs['class'] .= ' current';
}
$this->elementStart('li', $attrs);
@@ -669,10 +676,6 @@ class Action extends HTMLOutputter // lawsuit
$this->showContentBlock();
Event::handle('EndShowContentBlock', array($this));
}
- if (Event::handle('StartShowObjectNavBlock', array($this))) {
- $this->showObjectNavBlock();
- Event::handle('EndShowObjectNavBlock', array($this));
- }
if (Event::handle('StartShowAside', array($this))) {
$this->showAside();
Event::handle('EndShowAside', array($this));
@@ -710,15 +713,24 @@ class Action extends HTMLOutputter // lawsuit
/**
* Show menu for an object (group, profile)
*
+ * This block will only show if a subclass has overridden
+ * the showObjectNav() method.
+ *
* @return nothing
*/
function showObjectNavBlock()
{
- // Need to have this ID for CSS; I'm too lazy to add it to
- // all menus
- $this->elementStart('div', array('id' => 'site_nav_object'));
- $this->showObjectNav();
- $this->elementEnd('div');
+ $rmethod = new ReflectionMethod($this, 'showObjectNav');
+ $dclass = $rmethod->getDeclaringClass()->getName();
+
+ if ($dclass != 'Action') {
+ // Need to have this ID for CSS; I'm too lazy to add it to
+ // all menus
+ $this->elementStart('div', array('id' => 'site_nav_object',
+ 'class' => 'section'));
+ $this->showObjectNav();
+ $this->elementEnd('div');
+ }
}
/**
@@ -828,6 +840,10 @@ class Action extends HTMLOutputter // lawsuit
{
$this->elementStart('div', array('id' => 'aside_primary',
'class' => 'aside'));
+ if (Event::handle('StartShowObjectNavBlock', array($this))) {
+ $this->showObjectNavBlock();
+ Event::handle('EndShowObjectNavBlock', array($this));
+ }
if (Event::handle('StartShowSections', array($this))) {
$this->showSections();
Event::handle('EndShowSections', array($this));
diff --git a/lib/adminpanelnav.php b/lib/adminpanelnav.php
index ceedf6ceac..2c9d83ceba 100644
--- a/lib/adminpanelnav.php
+++ b/lib/adminpanelnav.php
@@ -56,7 +56,25 @@ class AdminPanelNav extends Menu
function show()
{
$action_name = $this->action->trimmed('action');
+ $user = common_current_user();
+ $nickname = $user->nickname;
+ $name = $user->getProfile()->getBestName();
+ // Stub section w/ home link
+ $this->action->elementStart('ul');
+ $this->action->element('h3', null, _('Home'));
+ $this->action->elementStart('ul', 'nav');
+ $this->out->menuItem(common_local_url('all', array('nickname' =>
+ $nickname)),
+ _('Home'),
+ sprintf(_('%s and friends'), $name),
+ $this->action == 'all', 'nav_timeline_personal');
+
+ $this->action->elementEnd('ul');
+ $this->action->elementEnd('ul');
+
+ $this->action->elementStart('ul');
+ $this->action->element('h3', null, _('Admin'));
$this->action->elementStart('ul', array('class' => 'nav'));
if (Event::handle('StartAdminPanelNav', array($this))) {
@@ -144,5 +162,6 @@ class AdminPanelNav extends Menu
Event::handle('EndAdminPanelNav', array($this));
}
$this->action->elementEnd('ul');
+ $this->action->elementEnd('ul');
}
}
diff --git a/lib/error.php b/lib/error.php
index 762425dc44..d234ab92b2 100644
--- a/lib/error.php
+++ b/lib/error.php
@@ -91,6 +91,7 @@ class ErrorAction extends InfoAction
$this->element('div', array('class' => 'error'), $this->message);
}
-
-
+ function showNoticeForm()
+ {
+ }
}
diff --git a/lib/logingroupnav.php b/lib/logingroupnav.php
index 3c67f76322..5d1b52f795 100644
--- a/lib/logingroupnav.php
+++ b/lib/logingroupnav.php
@@ -66,7 +66,8 @@ class LoginGroupNav extends Menu
_('Login with a username and password'),
$action_name === 'login');
- if (!(common_config('site','closed') || common_config('site','inviteonly'))) {
+ if (!common_logged_in() &&
+ !(common_config('site','closed') || common_config('site','inviteonly'))) {
$this->action->menuItem(common_local_url('register'),
// TRANS: Menu item for registering with the StatusNet site.
_m('MENU','Register'),
diff --git a/lib/messageform.php b/lib/messageform.php
index 9a4dfbb0f5..733e83cd15 100644
--- a/lib/messageform.php
+++ b/lib/messageform.php
@@ -96,7 +96,7 @@ class MessageForm extends Form
function formClass()
{
- return 'form_notice';
+ return 'form_notice ajax-notice';
}
/**
@@ -153,7 +153,7 @@ class MessageForm extends Form
$this->out->dropdown('to', _('To'), $mutual, null, false,
($this->to) ? $this->to->id : null);
- $this->out->element('textarea', array('id' => 'notice_data-text',
+ $this->out->element('textarea', array('class' => 'notice_data-text',
'cols' => 35,
'rows' => 4,
'name' => 'content'),
diff --git a/lib/noticeform.php b/lib/noticeform.php
index 271a37449c..9d931b92ed 100644
--- a/lib/noticeform.php
+++ b/lib/noticeform.php
@@ -132,7 +132,7 @@ class NoticeForm extends Form
function formClass()
{
- return 'form_notice';
+ return 'form_notice ajax-notice';
}
/**
@@ -170,7 +170,7 @@ class NoticeForm extends Form
// TRANS: Title for notice label. %s is the user's nickname.
sprintf(_('What\'s up, %s?'), $this->user->nickname));
// XXX: vary by defined max size
- $this->out->element('textarea', array('id' => 'notice_data-text',
+ $this->out->element('textarea', array('class' => 'notice_data-text',
'cols' => 35,
'rows' => 4,
'name' => 'status_textarea'),
diff --git a/lib/noticelist.php b/lib/noticelist.php
index 8c07f904c8..dbe2a0996f 100644
--- a/lib/noticelist.php
+++ b/lib/noticelist.php
@@ -129,592 +129,3 @@ class NoticeList extends Widget
}
}
-/**
- * widget for displaying a single notice
- *
- * This widget has the core smarts for showing a single notice: what to display,
- * where, and under which circumstances. Its key method is show(); this is a recipe
- * that calls all the other show*() methods to build up a single notice. The
- * ProfileNoticeListItem subclass, for example, overrides showAuthor() to skip
- * author info (since that's implicit by the data in the page).
- *
- * @category UI
- * @package StatusNet
- * @author Evan Prodromou
- * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
- * @link http://status.net/
- * @see NoticeList
- * @see ProfileNoticeListItem
- */
-
-class NoticeListItem extends Widget
-{
- /** The notice this item will show. */
-
- var $notice = null;
-
- /** The notice that was repeated. */
-
- var $repeat = null;
-
- /** The profile of the author of the notice, extracted once for convenience. */
-
- var $profile = null;
-
- /**
- * constructor
- *
- * Also initializes the profile attribute.
- *
- * @param Notice $notice The notice we'll display
- */
-
- function __construct($notice, $out=null)
- {
- parent::__construct($out);
- if (!empty($notice->repeat_of)) {
- $original = Notice::staticGet('id', $notice->repeat_of);
- if (empty($original)) { // could have been deleted
- $this->notice = $notice;
- } else {
- $this->notice = $original;
- $this->repeat = $notice;
- }
- } else {
- $this->notice = $notice;
- }
- $this->profile = $this->notice->getProfile();
- }
-
- /**
- * recipe function for displaying a single notice.
- *
- * This uses all the other methods to correctly display a notice. Override
- * it or one of the others to fine-tune the output.
- *
- * @return void
- */
-
- function show()
- {
- if (empty($this->notice)) {
- common_log(LOG_WARNING, "Trying to show missing notice; skipping.");
- return;
- } else if (empty($this->profile)) {
- common_log(LOG_WARNING, "Trying to show missing profile (" . $this->notice->profile_id . "); skipping.");
- return;
- }
-
- $this->showStart();
- if (Event::handle('StartShowNoticeItem', array($this))) {
- $this->showNotice();
- $this->showNoticeAttachments();
- $this->showNoticeInfo();
- $this->showNoticeOptions();
- Event::handle('EndShowNoticeItem', array($this));
- }
- $this->showEnd();
- }
-
- function showNotice()
- {
- $this->out->elementStart('div', 'entry-title');
- $this->showAuthor();
- $this->showContent();
- $this->out->elementEnd('div');
- }
-
- function showNoticeInfo()
- {
- $this->out->elementStart('div', 'entry-content');
- if (Event::handle('StartShowNoticeInfo', array($this))) {
- $this->showNoticeLink();
- $this->showNoticeSource();
- $this->showNoticeLocation();
- $this->showContext();
- $this->showRepeat();
- Event::handle('EndShowNoticeInfo', array($this));
- }
-
- $this->out->elementEnd('div');
- }
-
- function showNoticeOptions()
- {
- if (Event::handle('StartShowNoticeOptions', array($this))) {
- $user = common_current_user();
- if ($user) {
- $this->out->elementStart('div', 'notice-options');
- $this->showFaveForm();
- $this->showReplyLink();
- $this->showRepeatForm();
- $this->showDeleteLink();
- $this->out->elementEnd('div');
- }
- Event::handle('EndShowNoticeOptions', array($this));
- }
- }
-
- /**
- * start a single notice.
- *
- * @return void
- */
-
- function showStart()
- {
- if (Event::handle('StartOpenNoticeListItemElement', array($this))) {
- $id = (empty($this->repeat)) ? $this->notice->id : $this->repeat->id;
- $this->out->elementStart('li', array('class' => 'hentry notice',
- 'id' => 'notice-' . $id));
- Event::handle('EndOpenNoticeListItemElement', array($this));
- }
- }
-
- /**
- * show the "favorite" form
- *
- * @return void
- */
-
- function showFaveForm()
- {
- if (Event::handle('StartShowFaveForm', array($this))) {
- $user = common_current_user();
- if ($user) {
- if ($user->hasFave($this->notice)) {
- $disfavor = new DisfavorForm($this->out, $this->notice);
- $disfavor->show();
- } else {
- $favor = new FavorForm($this->out, $this->notice);
- $favor->show();
- }
- }
- Event::handle('EndShowFaveForm', array($this));
- }
- }
-
- /**
- * show the author of a notice
- *
- * By default, this shows the avatar and (linked) nickname of the author.
- *
- * @return void
- */
-
- function showAuthor()
- {
- $this->out->elementStart('span', 'vcard author');
- $attrs = array('href' => $this->profile->profileurl,
- 'class' => 'url');
- if (!empty($this->profile->fullname)) {
- $attrs['title'] = $this->profile->getFancyName();
- }
- $this->out->elementStart('a', $attrs);
- $this->showAvatar();
- $this->out->text(' ');
- $this->showNickname();
- $this->out->elementEnd('a');
- $this->out->elementEnd('span');
- }
-
- /**
- * show the avatar of the notice's author
- *
- * This will use the default avatar if no avatar is assigned for the author.
- * It makes a link to the author's profile.
- *
- * @return void
- */
-
- function showAvatar()
- {
- $avatar_size = $this->avatarSize();
-
- $avatar = $this->profile->getAvatar($avatar_size);
-
- $this->out->element('img', array('src' => ($avatar) ?
- $avatar->displayUrl() :
- Avatar::defaultImage($avatar_size),
- 'class' => 'avatar photo',
- 'width' => $avatar_size,
- 'height' => $avatar_size,
- 'alt' =>
- ($this->profile->fullname) ?
- $this->profile->fullname :
- $this->profile->nickname));
- }
-
- function avatarSize()
- {
- return AVATAR_STREAM_SIZE;
- }
-
- /**
- * show the nickname of the author
- *
- * Links to the author's profile page
- *
- * @return void
- */
-
- function showNickname()
- {
- $this->out->raw('' .
- htmlspecialchars($this->profile->nickname) .
- ' ');
- }
-
- /**
- * show the content of the notice
- *
- * Shows the content of the notice. This is pre-rendered for efficiency
- * at save time. Some very old notices might not be pre-rendered, so
- * they're rendered on the spot.
- *
- * @return void
- */
-
- function showContent()
- {
- // FIXME: URL, image, video, audio
- $this->out->elementStart('p', array('class' => 'entry-content'));
- if ($this->notice->rendered) {
- $this->out->raw($this->notice->rendered);
- } else {
- // XXX: may be some uncooked notices in the DB,
- // we cook them right now. This should probably disappear in future
- // versions (>> 0.4.x)
- $this->out->raw(common_render_content($this->notice->content, $this->notice));
- }
- $this->out->elementEnd('p');
- }
-
- function showNoticeAttachments() {
- if (common_config('attachments', 'show_thumbs')) {
- $al = new InlineAttachmentList($this->notice, $this->out);
- $al->show();
- }
- }
-
- /**
- * show the link to the main page for the notice
- *
- * Displays a link to the page for a notice, with "relative" time. Tries to
- * get remote notice URLs correct, but doesn't always succeed.
- *
- * @return void
- */
-
- function showNoticeLink()
- {
- $noticeurl = $this->notice->bestUrl();
-
- // above should always return an URL
-
- assert(!empty($noticeurl));
-
- $this->out->elementStart('a', array('rel' => 'bookmark',
- 'class' => 'timestamp',
- 'href' => $noticeurl));
- $dt = common_date_iso8601($this->notice->created);
- $this->out->element('abbr', array('class' => 'published',
- 'title' => $dt),
- common_date_string($this->notice->created));
- $this->out->elementEnd('a');
- }
-
- /**
- * show the notice location
- *
- * shows the notice location in the correct language.
- *
- * If an URL is available, makes a link. Otherwise, just a span.
- *
- * @return void
- */
-
- function showNoticeLocation()
- {
- $id = $this->notice->id;
-
- $location = $this->notice->getLocation();
-
- if (empty($location)) {
- return;
- }
-
- $name = $location->getName();
-
- $lat = $this->notice->lat;
- $lon = $this->notice->lon;
- $latlon = (!empty($lat) && !empty($lon)) ? $lat.';'.$lon : '';
-
- if (empty($name)) {
- $latdms = $this->decimalDegreesToDMS(abs($lat));
- $londms = $this->decimalDegreesToDMS(abs($lon));
- // TRANS: Used in coordinates as abbreviation of north
- $north = _('N');
- // TRANS: Used in coordinates as abbreviation of south
- $south = _('S');
- // TRANS: Used in coordinates as abbreviation of east
- $east = _('E');
- // TRANS: Used in coordinates as abbreviation of west
- $west = _('W');
- $name = sprintf(
- _('%1$u°%2$u\'%3$u"%4$s %5$u°%6$u\'%7$u"%8$s'),
- $latdms['deg'],$latdms['min'], $latdms['sec'],($lat>0? $north:$south),
- $londms['deg'],$londms['min'], $londms['sec'],($lon>0? $east:$west));
- }
-
- $url = $location->getUrl();
-
- $this->out->text(' ');
- $this->out->elementStart('span', array('class' => 'location'));
- $this->out->text(_('at'));
- $this->out->text(' ');
- if (empty($url)) {
- $this->out->element('abbr', array('class' => 'geo',
- 'title' => $latlon),
- $name);
- } else {
- $xstr = new XMLStringer(false);
- $xstr->elementStart('a', array('href' => $url,
- 'rel' => 'external'));
- $xstr->element('abbr', array('class' => 'geo',
- 'title' => $latlon),
- $name);
- $xstr->elementEnd('a');
- $this->out->raw($xstr->getString());
- }
- $this->out->elementEnd('span');
- }
-
- /**
- * @param number $dec decimal degrees
- * @return array split into 'deg', 'min', and 'sec'
- */
- function decimalDegreesToDMS($dec)
- {
- $deg = intval($dec);
- $tempma = abs($dec) - abs($deg);
-
- $tempma = $tempma * 3600;
- $min = floor($tempma / 60);
- $sec = $tempma - ($min*60);
-
- return array("deg"=>$deg,"min"=>$min,"sec"=>$sec);
- }
-
- /**
- * Show the source of the notice
- *
- * Either the name (and link) of the API client that posted the notice,
- * or one of other other channels.
- *
- * @return void
- */
-
- function showNoticeSource()
- {
- $ns = $this->notice->getSource();
-
- if ($ns) {
- $source_name = (empty($ns->name)) ? ($ns->code ? _($ns->code) : _('web')) : _($ns->name);
- $this->out->text(' ');
- $this->out->elementStart('span', 'source');
- // FIXME: probably i18n issue. If "from" is followed by text, that should be a parameter to "from" (from %s).
- $this->out->text(_('from'));
- $this->out->text(' ');
-
- $name = $source_name;
- $url = $ns->url;
- $title = null;
-
- if (Event::handle('StartNoticeSourceLink', array($this->notice, &$name, &$url, &$title))) {
- $name = $source_name;
- $url = $ns->url;
- }
- Event::handle('EndNoticeSourceLink', array($this->notice, &$name, &$url, &$title));
-
- // if $ns->name and $ns->url are populated we have
- // configured a source attr somewhere
- if (!empty($name) && !empty($url)) {
-
- $this->out->elementStart('span', 'device');
-
- $attrs = array(
- 'href' => $url,
- 'rel' => 'external'
- );
-
- if (!empty($title)) {
- $attrs['title'] = $title;
- }
-
- $this->out->element('a', $attrs, $name);
- $this->out->elementEnd('span');
- } else {
- $this->out->element('span', 'device', $name);
- }
-
- $this->out->elementEnd('span');
- }
- }
-
- /**
- * show link to notice this notice is a reply to
- *
- * If this notice is a reply, show a link to the notice it is replying to. The
- * heavy lifting for figuring out replies happens at save time.
- *
- * @return void
- */
-
- function showContext()
- {
- if ($this->notice->hasConversation()) {
- $conv = Conversation::staticGet(
- 'id',
- $this->notice->conversation
- );
- $convurl = $conv->uri;
- if (!empty($convurl)) {
- $this->out->text(' ');
- $this->out->element(
- 'a',
- array(
- 'href' => $convurl.'#notice-'.$this->notice->id,
- 'class' => 'response'),
- _('in context')
- );
- } else {
- $msg = sprintf(
- "Couldn't find Conversation ID %d to make 'in context'"
- . "link for Notice ID %d",
- $this->notice->conversation,
- $this->notice->id
- );
- common_log(LOG_WARNING, $msg);
- }
- }
- }
-
- /**
- * show a link to the author of repeat
- *
- * @return void
- */
-
- function showRepeat()
- {
- if (!empty($this->repeat)) {
-
- $repeater = Profile::staticGet('id', $this->repeat->profile_id);
-
- $attrs = array('href' => $repeater->profileurl,
- 'class' => 'url');
-
- if (!empty($repeater->fullname)) {
- $attrs['title'] = $repeater->fullname . ' (' . $repeater->nickname . ')';
- }
-
- $this->out->elementStart('span', 'repeat vcard');
-
- $this->out->raw(_('Repeated by'));
-
- $this->out->elementStart('a', $attrs);
- $this->out->element('span', 'fn nickname', $repeater->nickname);
- $this->out->elementEnd('a');
-
- $this->out->elementEnd('span');
- }
- }
-
- /**
- * show a link to reply to the current notice
- *
- * Should either do the reply in the current notice form (if available), or
- * link out to the notice-posting form. A little flakey, doesn't always work.
- *
- * @return void
- */
-
- function showReplyLink()
- {
- if (common_logged_in()) {
- $this->out->text(' ');
- $reply_url = common_local_url('newnotice',
- array('replyto' => $this->profile->nickname, 'inreplyto' => $this->notice->id));
- $this->out->elementStart('a', array('href' => $reply_url,
- 'class' => 'notice_reply',
- 'title' => _('Reply to this notice')));
- $this->out->text(_('Reply'));
- $this->out->text(' ');
- $this->out->element('span', 'notice_id', $this->notice->id);
- $this->out->elementEnd('a');
- }
- }
-
- /**
- * if the user is the author, let them delete the notice
- *
- * @return void
- */
-
- function showDeleteLink()
- {
- $user = common_current_user();
-
- $todel = (empty($this->repeat)) ? $this->notice : $this->repeat;
-
- if (!empty($user) &&
- ($todel->profile_id == $user->id || $user->hasRight(Right::DELETEOTHERSNOTICE))) {
- $this->out->text(' ');
- $deleteurl = common_local_url('deletenotice',
- array('notice' => $todel->id));
- $this->out->element('a', array('href' => $deleteurl,
- 'class' => 'notice_delete',
- 'title' => _('Delete this notice')), _('Delete'));
- }
- }
-
- /**
- * show the form to repeat a notice
- *
- * @return void
- */
-
- function showRepeatForm()
- {
- $user = common_current_user();
- if ($user && $user->id != $this->notice->profile_id) {
- $this->out->text(' ');
- $profile = $user->getProfile();
- if ($profile->hasRepeated($this->notice->id)) {
- $this->out->element('span', array('class' => 'repeated',
- 'title' => _('Notice repeated')),
- _('Repeated'));
- } else {
- $rf = new RepeatForm($this->out, $this->notice);
- $rf->show();
- }
- }
- }
-
- /**
- * finish the notice
- *
- * Close the last elements in the notice list item
- *
- * @return void
- */
-
- function showEnd()
- {
- if (Event::handle('StartCloseNoticeListItemElement', array($this))) {
- $this->out->elementEnd('li');
- Event::handle('EndCloseNoticeListItemElement', array($this));
- }
- }
-}
diff --git a/lib/noticelistitem.php b/lib/noticelistitem.php
new file mode 100644
index 0000000000..17827d07ef
--- /dev/null
+++ b/lib/noticelistitem.php
@@ -0,0 +1,625 @@
+.
+ *
+ * @category Widget
+ * @package StatusNet
+ * @author Evan Prodromou
+ * @copyright 2010 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
+ * @link http://status.net/
+ */
+
+if (!defined('STATUSNET')) {
+ // This check helps protect against security problems;
+ // your code file can't be executed directly from the web.
+ exit(1);
+}
+
+/**
+ * widget for displaying a single notice
+ *
+ * This widget has the core smarts for showing a single notice: what to display,
+ * where, and under which circumstances. Its key method is show(); this is a recipe
+ * that calls all the other show*() methods to build up a single notice. The
+ * ProfileNoticeListItem subclass, for example, overrides showAuthor() to skip
+ * author info (since that's implicit by the data in the page).
+ *
+ * @category UI
+ * @package StatusNet
+ * @author Evan Prodromou
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
+ * @link http://status.net/
+ * @see NoticeList
+ * @see ProfileNoticeListItem
+ */
+
+class NoticeListItem extends Widget
+{
+ /** The notice this item will show. */
+
+ var $notice = null;
+
+ /** The notice that was repeated. */
+
+ var $repeat = null;
+
+ /** The profile of the author of the notice, extracted once for convenience. */
+
+ var $profile = null;
+
+ /**
+ * constructor
+ *
+ * Also initializes the profile attribute.
+ *
+ * @param Notice $notice The notice we'll display
+ */
+
+ function __construct($notice, $out=null)
+ {
+ parent::__construct($out);
+ if (!empty($notice->repeat_of)) {
+ $original = Notice::staticGet('id', $notice->repeat_of);
+ if (empty($original)) { // could have been deleted
+ $this->notice = $notice;
+ } else {
+ $this->notice = $original;
+ $this->repeat = $notice;
+ }
+ } else {
+ $this->notice = $notice;
+ }
+ $this->profile = $this->notice->getProfile();
+ }
+
+ /**
+ * recipe function for displaying a single notice.
+ *
+ * This uses all the other methods to correctly display a notice. Override
+ * it or one of the others to fine-tune the output.
+ *
+ * @return void
+ */
+
+ function show()
+ {
+ if (empty($this->notice)) {
+ common_log(LOG_WARNING, "Trying to show missing notice; skipping.");
+ return;
+ } else if (empty($this->profile)) {
+ common_log(LOG_WARNING, "Trying to show missing profile (" . $this->notice->profile_id . "); skipping.");
+ return;
+ }
+
+ $this->showStart();
+ if (Event::handle('StartShowNoticeItem', array($this))) {
+ $this->showNotice();
+ $this->showNoticeAttachments();
+ $this->showNoticeInfo();
+ $this->showNoticeOptions();
+ Event::handle('EndShowNoticeItem', array($this));
+ }
+ $this->showEnd();
+ }
+
+ function showNotice()
+ {
+ $this->out->elementStart('div', 'entry-title');
+ $this->showAuthor();
+ $this->showContent();
+ $this->out->elementEnd('div');
+ }
+
+ function showNoticeInfo()
+ {
+ $this->out->elementStart('div', 'entry-content');
+ if (Event::handle('StartShowNoticeInfo', array($this))) {
+ $this->showNoticeLink();
+ $this->showNoticeSource();
+ $this->showNoticeLocation();
+ $this->showContext();
+ $this->showRepeat();
+ Event::handle('EndShowNoticeInfo', array($this));
+ }
+
+ $this->out->elementEnd('div');
+ }
+
+ function showNoticeOptions()
+ {
+ if (Event::handle('StartShowNoticeOptions', array($this))) {
+ $user = common_current_user();
+ if ($user) {
+ $this->out->elementStart('div', 'notice-options');
+ $this->showFaveForm();
+ $this->showReplyLink();
+ $this->showRepeatForm();
+ $this->showDeleteLink();
+ $this->out->elementEnd('div');
+ }
+ Event::handle('EndShowNoticeOptions', array($this));
+ }
+ }
+
+ /**
+ * start a single notice.
+ *
+ * @return void
+ */
+
+ function showStart()
+ {
+ if (Event::handle('StartOpenNoticeListItemElement', array($this))) {
+ $id = (empty($this->repeat)) ? $this->notice->id : $this->repeat->id;
+ $this->out->elementStart('li', array('class' => 'hentry notice',
+ 'id' => 'notice-' . $id));
+ Event::handle('EndOpenNoticeListItemElement', array($this));
+ }
+ }
+
+ /**
+ * show the "favorite" form
+ *
+ * @return void
+ */
+
+ function showFaveForm()
+ {
+ if (Event::handle('StartShowFaveForm', array($this))) {
+ $user = common_current_user();
+ if ($user) {
+ if ($user->hasFave($this->notice)) {
+ $disfavor = new DisfavorForm($this->out, $this->notice);
+ $disfavor->show();
+ } else {
+ $favor = new FavorForm($this->out, $this->notice);
+ $favor->show();
+ }
+ }
+ Event::handle('EndShowFaveForm', array($this));
+ }
+ }
+
+ /**
+ * show the author of a notice
+ *
+ * By default, this shows the avatar and (linked) nickname of the author.
+ *
+ * @return void
+ */
+
+ function showAuthor()
+ {
+ $this->out->elementStart('span', 'vcard author');
+ $attrs = array('href' => $this->profile->profileurl,
+ 'class' => 'url');
+ if (!empty($this->profile->fullname)) {
+ $attrs['title'] = $this->profile->getFancyName();
+ }
+ $this->out->elementStart('a', $attrs);
+ $this->showAvatar();
+ $this->out->text(' ');
+ $this->showNickname();
+ $this->out->elementEnd('a');
+ $this->out->elementEnd('span');
+ }
+
+ /**
+ * show the avatar of the notice's author
+ *
+ * This will use the default avatar if no avatar is assigned for the author.
+ * It makes a link to the author's profile.
+ *
+ * @return void
+ */
+
+ function showAvatar()
+ {
+ $avatar_size = $this->avatarSize();
+
+ $avatar = $this->profile->getAvatar($avatar_size);
+
+ $this->out->element('img', array('src' => ($avatar) ?
+ $avatar->displayUrl() :
+ Avatar::defaultImage($avatar_size),
+ 'class' => 'avatar photo',
+ 'width' => $avatar_size,
+ 'height' => $avatar_size,
+ 'alt' =>
+ ($this->profile->fullname) ?
+ $this->profile->fullname :
+ $this->profile->nickname));
+ }
+
+ function avatarSize()
+ {
+ return AVATAR_STREAM_SIZE;
+ }
+
+ /**
+ * show the nickname of the author
+ *
+ * Links to the author's profile page
+ *
+ * @return void
+ */
+
+ function showNickname()
+ {
+ $this->out->raw('' .
+ htmlspecialchars($this->profile->nickname) .
+ ' ');
+ }
+
+ /**
+ * show the content of the notice
+ *
+ * Shows the content of the notice. This is pre-rendered for efficiency
+ * at save time. Some very old notices might not be pre-rendered, so
+ * they're rendered on the spot.
+ *
+ * @return void
+ */
+
+ function showContent()
+ {
+ // FIXME: URL, image, video, audio
+ $this->out->elementStart('p', array('class' => 'entry-content'));
+ if ($this->notice->rendered) {
+ $this->out->raw($this->notice->rendered);
+ } else {
+ // XXX: may be some uncooked notices in the DB,
+ // we cook them right now. This should probably disappear in future
+ // versions (>> 0.4.x)
+ $this->out->raw(common_render_content($this->notice->content, $this->notice));
+ }
+ $this->out->elementEnd('p');
+ }
+
+ function showNoticeAttachments() {
+ if (common_config('attachments', 'show_thumbs')) {
+ $al = new InlineAttachmentList($this->notice, $this->out);
+ $al->show();
+ }
+ }
+
+ /**
+ * show the link to the main page for the notice
+ *
+ * Displays a link to the page for a notice, with "relative" time. Tries to
+ * get remote notice URLs correct, but doesn't always succeed.
+ *
+ * @return void
+ */
+
+ function showNoticeLink()
+ {
+ $noticeurl = $this->notice->bestUrl();
+
+ // above should always return an URL
+
+ assert(!empty($noticeurl));
+
+ $this->out->elementStart('a', array('rel' => 'bookmark',
+ 'class' => 'timestamp',
+ 'href' => $noticeurl));
+ $dt = common_date_iso8601($this->notice->created);
+ $this->out->element('abbr', array('class' => 'published',
+ 'title' => $dt),
+ common_date_string($this->notice->created));
+ $this->out->elementEnd('a');
+ }
+
+ /**
+ * show the notice location
+ *
+ * shows the notice location in the correct language.
+ *
+ * If an URL is available, makes a link. Otherwise, just a span.
+ *
+ * @return void
+ */
+
+ function showNoticeLocation()
+ {
+ $id = $this->notice->id;
+
+ $location = $this->notice->getLocation();
+
+ if (empty($location)) {
+ return;
+ }
+
+ $name = $location->getName();
+
+ $lat = $this->notice->lat;
+ $lon = $this->notice->lon;
+ $latlon = (!empty($lat) && !empty($lon)) ? $lat.';'.$lon : '';
+
+ if (empty($name)) {
+ $latdms = $this->decimalDegreesToDMS(abs($lat));
+ $londms = $this->decimalDegreesToDMS(abs($lon));
+ // TRANS: Used in coordinates as abbreviation of north
+ $north = _('N');
+ // TRANS: Used in coordinates as abbreviation of south
+ $south = _('S');
+ // TRANS: Used in coordinates as abbreviation of east
+ $east = _('E');
+ // TRANS: Used in coordinates as abbreviation of west
+ $west = _('W');
+ $name = sprintf(
+ _('%1$u°%2$u\'%3$u"%4$s %5$u°%6$u\'%7$u"%8$s'),
+ $latdms['deg'],$latdms['min'], $latdms['sec'],($lat>0? $north:$south),
+ $londms['deg'],$londms['min'], $londms['sec'],($lon>0? $east:$west));
+ }
+
+ $url = $location->getUrl();
+
+ $this->out->text(' ');
+ $this->out->elementStart('span', array('class' => 'location'));
+ $this->out->text(_('at'));
+ $this->out->text(' ');
+ if (empty($url)) {
+ $this->out->element('abbr', array('class' => 'geo',
+ 'title' => $latlon),
+ $name);
+ } else {
+ $xstr = new XMLStringer(false);
+ $xstr->elementStart('a', array('href' => $url,
+ 'rel' => 'external'));
+ $xstr->element('abbr', array('class' => 'geo',
+ 'title' => $latlon),
+ $name);
+ $xstr->elementEnd('a');
+ $this->out->raw($xstr->getString());
+ }
+ $this->out->elementEnd('span');
+ }
+
+ /**
+ * @param number $dec decimal degrees
+ * @return array split into 'deg', 'min', and 'sec'
+ */
+ function decimalDegreesToDMS($dec)
+ {
+ $deg = intval($dec);
+ $tempma = abs($dec) - abs($deg);
+
+ $tempma = $tempma * 3600;
+ $min = floor($tempma / 60);
+ $sec = $tempma - ($min*60);
+
+ return array("deg"=>$deg,"min"=>$min,"sec"=>$sec);
+ }
+
+ /**
+ * Show the source of the notice
+ *
+ * Either the name (and link) of the API client that posted the notice,
+ * or one of other other channels.
+ *
+ * @return void
+ */
+
+ function showNoticeSource()
+ {
+ $ns = $this->notice->getSource();
+
+ if ($ns) {
+ $source_name = (empty($ns->name)) ? ($ns->code ? _($ns->code) : _('web')) : _($ns->name);
+ $this->out->text(' ');
+ $this->out->elementStart('span', 'source');
+ // FIXME: probably i18n issue. If "from" is followed by text, that should be a parameter to "from" (from %s).
+ $this->out->text(_('from'));
+ $this->out->text(' ');
+
+ $name = $source_name;
+ $url = $ns->url;
+ $title = null;
+
+ if (Event::handle('StartNoticeSourceLink', array($this->notice, &$name, &$url, &$title))) {
+ $name = $source_name;
+ $url = $ns->url;
+ }
+ Event::handle('EndNoticeSourceLink', array($this->notice, &$name, &$url, &$title));
+
+ // if $ns->name and $ns->url are populated we have
+ // configured a source attr somewhere
+ if (!empty($name) && !empty($url)) {
+
+ $this->out->elementStart('span', 'device');
+
+ $attrs = array(
+ 'href' => $url,
+ 'rel' => 'external'
+ );
+
+ if (!empty($title)) {
+ $attrs['title'] = $title;
+ }
+
+ $this->out->element('a', $attrs, $name);
+ $this->out->elementEnd('span');
+ } else {
+ $this->out->element('span', 'device', $name);
+ }
+
+ $this->out->elementEnd('span');
+ }
+ }
+
+ /**
+ * show link to notice this notice is a reply to
+ *
+ * If this notice is a reply, show a link to the notice it is replying to. The
+ * heavy lifting for figuring out replies happens at save time.
+ *
+ * @return void
+ */
+
+ function showContext()
+ {
+ if ($this->notice->hasConversation()) {
+ $conv = Conversation::staticGet(
+ 'id',
+ $this->notice->conversation
+ );
+ $convurl = $conv->uri;
+ if (!empty($convurl)) {
+ $this->out->text(' ');
+ $this->out->element(
+ 'a',
+ array(
+ 'href' => $convurl.'#notice-'.$this->notice->id,
+ 'class' => 'response'),
+ _('in context')
+ );
+ } else {
+ $msg = sprintf(
+ "Couldn't find Conversation ID %d to make 'in context'"
+ . "link for Notice ID %d",
+ $this->notice->conversation,
+ $this->notice->id
+ );
+ common_log(LOG_WARNING, $msg);
+ }
+ }
+ }
+
+ /**
+ * show a link to the author of repeat
+ *
+ * @return void
+ */
+
+ function showRepeat()
+ {
+ if (!empty($this->repeat)) {
+
+ $repeater = Profile::staticGet('id', $this->repeat->profile_id);
+
+ $attrs = array('href' => $repeater->profileurl,
+ 'class' => 'url');
+
+ if (!empty($repeater->fullname)) {
+ $attrs['title'] = $repeater->fullname . ' (' . $repeater->nickname . ')';
+ }
+
+ $this->out->elementStart('span', 'repeat vcard');
+
+ $this->out->raw(_('Repeated by'));
+
+ $this->out->elementStart('a', $attrs);
+ $this->out->element('span', 'fn nickname', $repeater->nickname);
+ $this->out->elementEnd('a');
+
+ $this->out->elementEnd('span');
+ }
+ }
+
+ /**
+ * show a link to reply to the current notice
+ *
+ * Should either do the reply in the current notice form (if available), or
+ * link out to the notice-posting form. A little flakey, doesn't always work.
+ *
+ * @return void
+ */
+
+ function showReplyLink()
+ {
+ if (common_logged_in()) {
+ $this->out->text(' ');
+ $reply_url = common_local_url('newnotice',
+ array('replyto' => $this->profile->nickname, 'inreplyto' => $this->notice->id));
+ $this->out->elementStart('a', array('href' => $reply_url,
+ 'class' => 'notice_reply',
+ 'title' => _('Reply to this notice')));
+ $this->out->text(_('Reply'));
+ $this->out->text(' ');
+ $this->out->element('span', 'notice_id', $this->notice->id);
+ $this->out->elementEnd('a');
+ }
+ }
+
+ /**
+ * if the user is the author, let them delete the notice
+ *
+ * @return void
+ */
+
+ function showDeleteLink()
+ {
+ $user = common_current_user();
+
+ $todel = (empty($this->repeat)) ? $this->notice : $this->repeat;
+
+ if (!empty($user) &&
+ ($todel->profile_id == $user->id || $user->hasRight(Right::DELETEOTHERSNOTICE))) {
+ $this->out->text(' ');
+ $deleteurl = common_local_url('deletenotice',
+ array('notice' => $todel->id));
+ $this->out->element('a', array('href' => $deleteurl,
+ 'class' => 'notice_delete',
+ 'title' => _('Delete this notice')), _('Delete'));
+ }
+ }
+
+ /**
+ * show the form to repeat a notice
+ *
+ * @return void
+ */
+
+ function showRepeatForm()
+ {
+ $user = common_current_user();
+ if ($user && $user->id != $this->notice->profile_id) {
+ $this->out->text(' ');
+ $profile = $user->getProfile();
+ if ($profile->hasRepeated($this->notice->id)) {
+ $this->out->element('span', array('class' => 'repeated',
+ 'title' => _('Notice repeated')),
+ _('Repeated'));
+ } else {
+ $rf = new RepeatForm($this->out, $this->notice);
+ $rf->show();
+ }
+ }
+ }
+
+ /**
+ * finish the notice
+ *
+ * Close the last elements in the notice list item
+ *
+ * @return void
+ */
+
+ function showEnd()
+ {
+ if (Event::handle('StartCloseNoticeListItemElement', array($this))) {
+ $this->out->elementEnd('li');
+ Event::handle('EndCloseNoticeListItemElement', array($this));
+ }
+ }
+}
diff --git a/lib/settingsnav.php b/lib/settingsnav.php
index 697e7ee46b..2987e36ea9 100644
--- a/lib/settingsnav.php
+++ b/lib/settingsnav.php
@@ -57,6 +57,25 @@ class SettingsNav extends Menu
function show()
{
$actionName = $this->action->trimmed('action');
+ $user = common_current_user();
+ $nickname = $user->nickname;
+ $name = $user->getProfile()->getBestName();
+
+ // Stub section w/ home link
+ $this->action->elementStart('ul');
+ $this->action->element('h3', null, _('Home'));
+ $this->action->elementStart('ul', 'nav');
+ $this->out->menuItem(common_local_url('all', array('nickname' =>
+ $nickname)),
+ _('Home'),
+ sprintf(_('%s and friends'), $name),
+ $this->action == 'all', 'nav_timeline_personal');
+
+ $this->action->elementEnd('ul');
+ $this->action->elementEnd('ul');
+
+ $this->action->elementStart('ul');
+ $this->action->element('h3', null, _('Settings'));
$this->action->elementStart('ul', array('class' => 'nav'));
if (Event::handle('StartAccountSettingsNav', array(&$this->action))) {
@@ -115,5 +134,6 @@ class SettingsNav extends Menu
}
$this->action->elementEnd('ul');
+ $this->action->elementEnd('ul');
}
}
diff --git a/lib/theme.php b/lib/theme.php
index 5caa046c20..b5f2b58cf2 100644
--- a/lib/theme.php
+++ b/lib/theme.php
@@ -56,6 +56,9 @@ class Theme
var $name = null;
var $dir = null;
var $path = null;
+ protected $metadata = null; // access via getMetadata() lazy-loader
+ protected $externals = null;
+ protected $deps = null;
/**
* Constructor
@@ -199,9 +202,12 @@ class Theme
*/
function getDeps()
{
- $chain = $this->doGetDeps(array($this->name));
- array_pop($chain); // Drop us back off
- return $chain;
+ if ($this->deps === null) {
+ $chain = $this->doGetDeps(array($this->name));
+ array_pop($chain); // Drop us back off
+ $this->deps = $chain;
+ }
+ return $this->deps;
}
protected function doGetDeps($chain)
@@ -233,6 +239,20 @@ class Theme
* @return associative array of strings
*/
function getMetadata()
+ {
+ if ($this->metadata == null) {
+ $this->metadata = $this->doGetMetadata();
+ }
+ return $this->metadata;
+ }
+
+ /**
+ * Pull data from the theme's theme.ini file.
+ * @fixme calling getFile will fall back to default theme, this may be unsafe.
+ *
+ * @return associative array of strings
+ */
+ private function doGetMetadata()
{
$iniFile = $this->getFile('theme.ini');
if (file_exists($iniFile)) {
@@ -242,6 +262,32 @@ class Theme
}
}
+ /**
+ * Get list of any external URLs required by this theme and any
+ * dependencies. These are lazy-loaded from theme.ini.
+ *
+ * @return array of URL strings
+ */
+ function getExternals()
+ {
+ if ($this->externals == null) {
+ $data = $this->getMetadata();
+ if (!empty($data['external'])) {
+ $ext = (array)$data['external'];
+ } else {
+ $ext = array();
+ }
+
+ if (!empty($data['include'])) {
+ $theme = new Theme($data['include']);
+ $ext = array_merge($ext, $theme->getExternals());
+ }
+
+ $this->externals = array_unique($ext);
+ }
+ return $this->externals;
+ }
+
/**
* Gets the full path of a file in a theme dir based on its relative name
*
diff --git a/plugins/Bookmark/BookmarkPlugin.php b/plugins/Bookmark/BookmarkPlugin.php
index 6c3f8cdc28..bc8985e907 100644
--- a/plugins/Bookmark/BookmarkPlugin.php
+++ b/plugins/Bookmark/BookmarkPlugin.php
@@ -616,12 +616,15 @@ class BookmarkPlugin extends MicroAppPlugin
'height' => AVATAR_MINI_SIZE,
'alt' => $profile->getBestName()));
- $out->raw(' ');
+ $out->raw(' '); // avoid for AJAX XML compatibility
+ $out->elementStart('span', 'vcard author'); // hack for belongsOnTimeline; JS needs to be able to find the author
$out->element('a',
- array('href' => $profile->profileurl,
+ array('class' => 'url',
+ 'href' => $profile->profileurl,
'title' => $profile->getBestName()),
$profile->nickname);
+ $out->elementEnd('span');
}
function entryForm($out)
diff --git a/plugins/Bookmark/bookmarkform.php b/plugins/Bookmark/bookmarkform.php
index b99568e154..d8cf1f7f5b 100644
--- a/plugins/Bookmark/bookmarkform.php
+++ b/plugins/Bookmark/bookmarkform.php
@@ -94,7 +94,7 @@ class BookmarkForm extends Form
function formClass()
{
- return 'form_settings';
+ return 'form_settings ajax-notice';
}
/**
diff --git a/plugins/Bookmark/newbookmark.php b/plugins/Bookmark/newbookmark.php
index a0cf3fffb2..ebfdb6cb95 100644
--- a/plugins/Bookmark/newbookmark.php
+++ b/plugins/Bookmark/newbookmark.php
@@ -125,6 +125,9 @@ class NewbookmarkAction extends Action
function newBookmark()
{
+ if ($this->boolean('ajax')) {
+ StatusNet::setApi(true);
+ }
try {
if (empty($this->title)) {
throw new ClientException(_('Bookmark must have a title.'));
@@ -147,7 +150,37 @@ class NewbookmarkAction extends Action
return;
}
- common_redirect($saved->bestUrl(), 303);
+ if ($this->boolean('ajax')) {
+ header('Content-Type: text/xml;charset=utf-8');
+ $this->xw->startDocument('1.0', 'UTF-8');
+ $this->elementStart('html');
+ $this->elementStart('head');
+ // TRANS: Page title after sending a notice.
+ $this->element('title', null, _('Notice posted'));
+ $this->elementEnd('head');
+ $this->elementStart('body');
+ $this->showNotice($saved);
+ $this->elementEnd('body');
+ $this->elementEnd('html');
+ } else {
+ common_redirect($saved->bestUrl(), 303);
+ }
+ }
+
+ /**
+ * Output a notice
+ *
+ * Used to generate the notice code for Ajax results.
+ *
+ * @param Notice $notice Notice that was saved
+ *
+ * @return void
+ */
+ function showNotice($notice)
+ {
+ class_exists('NoticeList'); // @fixme hack for autoloader
+ $nli = new NoticeListItem($notice, $this);
+ $nli->show();
}
/**
diff --git a/plugins/Event/EventPlugin.php b/plugins/Event/EventPlugin.php
new file mode 100644
index 0000000000..5c2fd35d74
--- /dev/null
+++ b/plugins/Event/EventPlugin.php
@@ -0,0 +1,438 @@
+.
+ *
+ * @category Event
+ * @package StatusNet
+ * @author Evan Prodromou
+ * @copyright 2011 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
+ * @link http://status.net/
+ */
+
+if (!defined('STATUSNET')) {
+ // This check helps protect against security problems;
+ // your code file can't be executed directly from the web.
+ exit(1);
+}
+
+/**
+ * Event plugin
+ *
+ * @category Sample
+ * @package StatusNet
+ * @author Evan Prodromou
+ * @copyright 2011 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
+ * @link http://status.net/
+ */
+class EventPlugin extends MicroappPlugin
+{
+ /**
+ * Set up our tables (event and rsvp)
+ *
+ * @see Schema
+ * @see ColumnDef
+ *
+ * @return boolean hook value; true means continue processing, false means stop.
+ */
+ function onCheckSchema()
+ {
+ $schema = Schema::get();
+
+ $schema->ensureTable('happening', Happening::schemaDef());
+ $schema->ensureTable('rsvp', RSVP::schemaDef());
+
+ return true;
+ }
+
+ /**
+ * Load related modules when needed
+ *
+ * @param string $cls Name of the class to be loaded
+ *
+ * @return boolean hook value; true means continue processing, false means stop.
+ */
+ function onAutoload($cls)
+ {
+ $dir = dirname(__FILE__);
+
+ switch ($cls)
+ {
+ case 'NeweventAction':
+ case 'NewrsvpAction':
+ case 'CancelrsvpAction':
+ case 'ShoweventAction':
+ case 'ShowrsvpAction':
+ include_once $dir . '/' . strtolower(mb_substr($cls, 0, -6)) . '.php';
+ return false;
+ case 'EventForm':
+ case 'RSVPForm':
+ case 'CancelRSVPForm':
+ include_once $dir . '/'.strtolower($cls).'.php';
+ break;
+ case 'Happening':
+ case 'RSVP':
+ include_once $dir . '/'.$cls.'.php';
+ return false;
+ default:
+ return true;
+ }
+ }
+
+ /**
+ * Map URLs to actions
+ *
+ * @param Net_URL_Mapper $m path-to-action mapper
+ *
+ * @return boolean hook value; true means continue processing, false means stop.
+ */
+
+ function onRouterInitialized($m)
+ {
+ $m->connect('main/event/new',
+ array('action' => 'newevent'));
+ $m->connect('main/event/rsvp',
+ array('action' => 'newrsvp'));
+ $m->connect('main/event/rsvp/cancel',
+ array('action' => 'cancelrsvp'));
+ $m->connect('event/:id',
+ array('action' => 'showevent'),
+ array('id' => '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}'));
+ $m->connect('rsvp/:id',
+ array('action' => 'showrsvp'),
+ array('id' => '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}'));
+ return true;
+ }
+
+ function onPluginVersion(&$versions)
+ {
+ $versions[] = array('name' => 'Event',
+ 'version' => STATUSNET_VERSION,
+ 'author' => 'Evan Prodromou',
+ 'homepage' => 'http://status.net/wiki/Plugin:Event',
+ 'description' =>
+ _m('Event invitations and RSVPs.'));
+ return true;
+ }
+
+ function appTitle() {
+ return _m('Event');
+ }
+
+ function tag() {
+ return 'event';
+ }
+
+ function types() {
+ return array(Happening::OBJECT_TYPE,
+ RSVP::POSITIVE,
+ RSVP::NEGATIVE,
+ RSVP::POSSIBLE);
+ }
+
+ /**
+ * Given a parsed ActivityStreams activity, save it into a notice
+ * and other data structures.
+ *
+ * @param Activity $activity
+ * @param Profile $actor
+ * @param array $options=array()
+ *
+ * @return Notice the resulting notice
+ */
+ function saveNoticeFromActivity($activity, $actor, $options=array())
+ {
+ if (count($activity->objects) != 1) {
+ throw new Exception('Too many activity objects.');
+ }
+
+ $happeningObj = $activity->objects[0];
+
+ if ($happeningObj->type != Happening::OBJECT_TYPE) {
+ throw new Exception('Wrong type for object.');
+ }
+
+ $notice = null;
+
+ switch ($activity->verb) {
+ case ActivityVerb::POST:
+ $notice = Happening::saveNew($actor,
+ $start_time,
+ $end_time,
+ $happeningObj->title,
+ null,
+ $happeningObj->summary,
+ $options);
+ break;
+ case RSVP::POSITIVE:
+ case RSVP::NEGATIVE:
+ case RSVP::POSSIBLE:
+ $happening = Happening::staticGet('uri', $happeningObj->id);
+ if (empty($happening)) {
+ // FIXME: save the event
+ throw new Exception("RSVP for unknown event.");
+ }
+ $notice = RSVP::saveNew($actor, $happening, $activity->verb, $options);
+ break;
+ default:
+ throw new Exception("Unknown verb for events");
+ }
+
+ return $notice;
+ }
+
+ /**
+ * Turn a Notice into an activity object
+ *
+ * @param Notice $notice
+ *
+ * @return ActivityObject
+ */
+
+ function activityObjectFromNotice($notice)
+ {
+ $happening = null;
+
+ switch ($notice->object_type) {
+ case Happening::OBJECT_TYPE:
+ $happening = Happening::fromNotice($notice);
+ break;
+ case RSVP::POSITIVE:
+ case RSVP::NEGATIVE:
+ case RSVP::POSSIBLE:
+ $rsvp = RSVP::fromNotice($notice);
+ $happening = $rsvp->getEvent();
+ break;
+ }
+
+ if (empty($happening)) {
+ throw new Exception("Unknown object type.");
+ }
+
+ $notice = $happening->getNotice();
+
+ if (empty($notice)) {
+ throw new Exception("Unknown event notice.");
+ }
+
+ $obj = new ActivityObject();
+
+ $obj->id = $happening->uri;
+ $obj->type = Happening::OBJECT_TYPE;
+ $obj->title = $happening->title;
+ $obj->summary = $happening->description;
+ $obj->link = $notice->bestUrl();
+
+ // XXX: how to get this stuff into JSON?!
+
+ $obj->extra[] = array('dtstart',
+ array('xmlns' => 'urn:ietf:params:xml:ns:xcal'),
+ common_date_iso8601($happening->start_date));
+
+ $obj->extra[] = array('dtend',
+ array('xmlns' => 'urn:ietf:params:xml:ns:xcal'),
+ common_date_iso8601($happening->end_date));
+
+ // XXX: probably need other stuff here
+
+ return $obj;
+ }
+
+ /**
+ * Change the verb on RSVP notices
+ *
+ * @param Notice $notice
+ *
+ * @return ActivityObject
+ */
+
+ function onEndNoticeAsActivity($notice, &$act) {
+ switch ($notice->object_type) {
+ case RSVP::POSITIVE:
+ case RSVP::NEGATIVE:
+ case RSVP::POSSIBLE:
+ $act->verb = $notice->object_type;
+ break;
+ }
+ return true;
+ }
+
+ /**
+ * Custom HTML output for our notices
+ *
+ * @param Notice $notice
+ * @param HTMLOutputter $out
+ */
+
+ function showNotice($notice, $out)
+ {
+ switch ($notice->object_type) {
+ case Happening::OBJECT_TYPE:
+ $this->showEventNotice($notice, $out);
+ break;
+ case RSVP::POSITIVE:
+ case RSVP::NEGATIVE:
+ case RSVP::POSSIBLE:
+ $this->showRSVPNotice($notice, $out);
+ break;
+ }
+
+ // @fixme we have to start the name/avatar and open this div
+ $out->elementStart('div', array('class' => 'event-info entry-content')); // EVENT-INFO.ENTRY-CONTENT IN
+
+ $profile = $notice->getProfile();
+ $avatar = $profile->getAvatar(AVATAR_MINI_SIZE);
+
+ $out->element('img',
+ array('src' => ($avatar) ?
+ $avatar->displayUrl() :
+ Avatar::defaultImage(AVATAR_MINI_SIZE),
+ 'class' => 'avatar photo bookmark-avatar',
+ 'width' => AVATAR_MINI_SIZE,
+ 'height' => AVATAR_MINI_SIZE,
+ 'alt' => $profile->getBestName()));
+
+ $out->raw(' '); // avoid for AJAX XML compatibility
+
+ $out->elementStart('span', 'vcard author'); // hack for belongsOnTimeline; JS needs to be able to find the author
+ $out->element('a',
+ array('class' => 'url',
+ 'href' => $profile->profileurl,
+ 'title' => $profile->getBestName()),
+ $profile->nickname);
+ $out->elementEnd('span');
+ }
+
+ function showRSVPNotice($notice, $out)
+ {
+ $out->raw($notice->rendered);
+ return;
+ }
+
+ function showEventNotice($notice, $out)
+ {
+ $profile = $notice->getProfile();
+ $event = Happening::fromNotice($notice);
+
+ assert(!empty($event));
+ assert(!empty($profile));
+
+ $out->elementStart('div', 'vevent'); // VEVENT IN
+
+ $out->elementStart('h3'); // VEVENT/H3 IN
+
+ if (!empty($event->url)) {
+ $out->element('a',
+ array('href' => $event->url,
+ 'class' => 'event-title entry-title summary'),
+ $event->title);
+ } else {
+ $out->text($event->title);
+ }
+
+ $out->elementEnd('h3'); // VEVENT/H3 OUT
+
+ // FIXME: better dates
+
+ $out->elementStart('div', 'event-times'); // VEVENT/EVENT-TIMES IN
+ $out->element('abbr', array('class' => 'dtstart',
+ 'title' => common_date_iso8601($event->start_time)),
+ common_exact_date($event->start_time));
+ $out->text(' - ');
+ $out->element('span', array('class' => 'dtend',
+ 'title' => common_date_iso8601($event->end_time)),
+ common_exact_date($event->end_time));
+ $out->elementEnd('div'); // VEVENT/EVENT-TIMES OUT
+
+ if (!empty($event->description)) {
+ $out->element('div', 'description', $event->description);
+ }
+
+ if (!empty($event->location)) {
+ $out->element('div', 'location', $event->location);
+ }
+
+ $rsvps = $event->getRSVPs();
+
+ $out->element('div', 'event-rsvps',
+ sprintf(_('Yes: %d No: %d Maybe: %d'),
+ count($rsvps[RSVP::POSITIVE]),
+ count($rsvps[RSVP::NEGATIVE]),
+ count($rsvps[RSVP::POSSIBLE])));
+
+ $user = common_current_user();
+
+ if (!empty($user)) {
+ $rsvp = $event->getRSVP($user->getProfile());
+ common_log(LOG_DEBUG, "RSVP is: " . ($rsvp ? $rsvp->id : 'none'));
+
+ if (empty($rsvp)) {
+ $form = new RSVPForm($event, $out);
+ } else {
+ $form = new CancelRSVPForm($rsvp, $out);
+ }
+
+ $form->show();
+ }
+
+ $out->elementEnd('div'); // vevent out
+ }
+
+ /**
+ * Form for our app
+ *
+ * @param HTMLOutputter $out
+ * @return Widget
+ */
+
+ function entryForm($out)
+ {
+ return new EventForm($out);
+ }
+
+ /**
+ * When a notice is deleted, clean up related tables.
+ *
+ * @param Notice $notice
+ */
+
+ function deleteRelated($notice)
+ {
+ switch ($notice->object_type) {
+ case Happening::OBJECT_TYPE:
+ common_log(LOG_DEBUG, "Deleting event from notice...");
+ $happening = Happening::fromNotice($notice);
+ $happening->delete();
+ break;
+ case RSVP::POSITIVE:
+ case RSVP::NEGATIVE:
+ case RSVP::POSSIBLE:
+ common_log(LOG_DEBUG, "Deleting rsvp from notice...");
+ $rsvp = RSVP::fromNotice($notice);
+ common_log(LOG_DEBUG, "to delete: $rsvp->id");
+ $rsvp->delete();
+ break;
+ default:
+ common_log(LOG_DEBUG, "Not deleting related, wtf...");
+ }
+ }
+}
diff --git a/plugins/Event/Happening.php b/plugins/Event/Happening.php
new file mode 100644
index 0000000000..376f27c698
--- /dev/null
+++ b/plugins/Event/Happening.php
@@ -0,0 +1,220 @@
+
+ * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3
+ * @link http://status.net/
+ *
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2011, StatusNet, Inc.
+ *
+ * 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 .
+ */
+
+if (!defined('STATUSNET')) {
+ exit(1);
+}
+
+/**
+ * Data class for happenings
+ *
+ * There's already an Event class in lib/event.php, so we couldn't
+ * call this an Event without causing a hole in space-time.
+ *
+ * "Happening" seemed good enough.
+ *
+ * @category Event
+ * @package StatusNet
+ * @author Evan Prodromou
+ * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3
+ * @link http://status.net/
+ *
+ * @see Managed_DataObject
+ */
+
+class Happening extends Managed_DataObject
+{
+ const OBJECT_TYPE = 'http://activitystrea.ms/schema/1.0/event';
+
+ public $__table = 'happening'; // table name
+ public $id; // varchar(36) UUID
+ public $uri; // varchar(255)
+ public $profile_id; // int
+ public $start_time; // datetime
+ public $end_time; // datetime
+ public $title; // varchar(255)
+ public $location; // varchar(255)
+ public $url; // varchar(255)
+ public $description; // text
+ public $created; // datetime
+
+ /**
+ * Get an instance by key
+ *
+ * @param string $k Key to use to lookup (usually 'id' for this class)
+ * @param mixed $v Value to lookup
+ *
+ * @return Happening object found, or null for no hits
+ *
+ */
+ function staticGet($k, $v=null)
+ {
+ return Memcached_DataObject::staticGet('Happening', $k, $v);
+ }
+
+ /**
+ * The One True Thingy that must be defined and declared.
+ */
+ public static function schemaDef()
+ {
+ return array(
+ 'description' => 'A real-world happening',
+ 'fields' => array(
+ 'id' => array('type' => 'char',
+ 'length' => 36,
+ 'not null' => true,
+ 'description' => 'UUID'),
+ 'uri' => array('type' => 'varchar',
+ 'length' => 255,
+ 'not null' => true),
+ 'profile_id' => array('type' => 'int', 'not null' => true),
+ 'start_time' => array('type' => 'datetime', 'not null' => true),
+ 'end_time' => array('type' => 'datetime', 'not null' => true),
+ 'title' => array('type' => 'varchar',
+ 'length' => 255,
+ 'not null' => true),
+ 'location' => array('type' => 'varchar',
+ 'length' => 255),
+ 'url' => array('type' => 'varchar',
+ 'length' => 255),
+ 'description' => array('type' => 'text'),
+ 'created' => array('type' => 'datetime',
+ 'not null' => true),
+ ),
+ 'primary key' => array('id'),
+ 'unique keys' => array(
+ 'happening_uri_key' => array('uri'),
+ ),
+ 'foreign keys' => array('happening_profile_id__key' => array('profile', array('profile_id' => 'id'))),
+ 'indexes' => array('happening_created_idx' => array('created'),
+ 'happening_start_end_idx' => array('start_time', 'end_time')),
+ );
+ }
+
+ function saveNew($profile, $start_time, $end_time, $title, $location, $description, $url, $options=array())
+ {
+ if (array_key_exists('uri', $options)) {
+ $other = Happening::staticGet('uri', $options['uri']);
+ if (!empty($other)) {
+ throw new ClientException(_('Event already exists.'));
+ }
+ }
+
+ $ev = new Happening();
+
+ $ev->id = UUID::gen();
+ $ev->profile_id = $profile->id;
+ $ev->start_time = common_sql_date($start_time);
+ $ev->end_time = common_sql_date($end_time);
+ $ev->title = $title;
+ $ev->location = $location;
+ $ev->description = $description;
+ $ev->url = $url;
+
+ if (array_key_exists('created', $options)) {
+ $ev->created = $options['created'];
+ } else {
+ $ev->created = common_sql_now();
+ }
+
+ if (array_key_exists('uri', $options)) {
+ $ev->uri = $options['uri'];
+ } else {
+ $ev->uri = common_local_url('showevent',
+ array('id' => $ev->id));
+ }
+
+ $ev->insert();
+
+ // XXX: does this get truncated?
+
+ $content = sprintf(_('"%s" %s - %s (%s): %s'),
+ $title,
+ common_exact_date($start_time),
+ common_exact_date($end_time),
+ $location,
+ $description);
+
+ $rendered = sprintf(_(''.
+ '%s '.
+ '%s - '.
+ '%s '.
+ '(%s ): '.
+ '%s '.
+ ' '),
+ htmlspecialchars($title),
+ htmlspecialchars(common_date_iso8601($start_time)),
+ htmlspecialchars(common_exact_date($start_time)),
+ htmlspecialchars(common_date_iso8601($end_time)),
+ htmlspecialchars(common_exact_date($end_time)),
+ htmlspecialchars($location),
+ htmlspecialchars($description));
+
+ $options = array_merge(array('object_type' => Happening::OBJECT_TYPE),
+ $options);
+
+ if (!array_key_exists('uri', $options)) {
+ $options['uri'] = $ev->uri;
+ }
+
+ if (!empty($url)) {
+ $options['urls'] = array($url);
+ }
+
+ $saved = Notice::saveNew($profile->id,
+ $content,
+ array_key_exists('source', $options) ?
+ $options['source'] : 'web',
+ $options);
+
+ return $saved;
+ }
+
+ function getNotice()
+ {
+ return Notice::staticGet('uri', $this->uri);
+ }
+
+ static function fromNotice($notice)
+ {
+ return Happening::staticGet('uri', $notice->uri);
+ }
+
+ function getRSVPs()
+ {
+ return RSVP::forEvent($this);
+ }
+
+ function getRSVP($profile)
+ {
+ common_log(LOG_DEBUG, "Finding RSVP for " . $profile->id . ', ' . $this->id);
+ return RSVP::pkeyGet(array('profile_id' => $profile->id,
+ 'event_id' => $this->id));
+ }
+}
diff --git a/plugins/Event/RSVP.php b/plugins/Event/RSVP.php
new file mode 100644
index 0000000000..c61ff3dbf0
--- /dev/null
+++ b/plugins/Event/RSVP.php
@@ -0,0 +1,249 @@
+
+ * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3
+ * @link http://status.net/
+ *
+ * StatusNet - the distributed open-source microblogging tool
+ * Copyright (C) 2011, StatusNet, Inc.
+ *
+ * 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 .
+ */
+
+if (!defined('STATUSNET')) {
+ exit(1);
+}
+
+/**
+ * Data class for event RSVPs
+ *
+ * @category Event
+ * @package StatusNet
+ * @author Evan Prodromou
+ * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3
+ * @link http://status.net/
+ *
+ * @see Managed_DataObject
+ */
+
+class RSVP extends Managed_DataObject
+{
+ const POSITIVE = 'http://activitystrea.ms/schema/1.0/rsvp-yes';
+ const POSSIBLE = 'http://activitystrea.ms/schema/1.0/rsvp-maybe';
+ const NEGATIVE = 'http://activitystrea.ms/schema/1.0/rsvp-no';
+
+ public $__table = 'rsvp'; // table name
+ public $id; // varchar(36) UUID
+ public $uri; // varchar(255)
+ public $profile_id; // int
+ public $event_id; // varchar(36) UUID
+ public $result; // tinyint
+ public $created; // datetime
+
+ /**
+ * Get an instance by key
+ *
+ * @param string $k Key to use to lookup (usually 'id' for this class)
+ * @param mixed $v Value to lookup
+ *
+ * @return RSVP object found, or null for no hits
+ *
+ */
+ function staticGet($k, $v=null)
+ {
+ return Memcached_DataObject::staticGet('RSVP', $k, $v);
+ }
+
+ /**
+ * Get an instance by compound key
+ *
+ * @param array $kv array of key-value mappings
+ *
+ * @return Bookmark object found, or null for no hits
+ *
+ */
+
+ function pkeyGet($kv)
+ {
+ return Memcached_DataObject::pkeyGet('RSVP', $kv);
+ }
+
+ /**
+ * Add the compound profile_id/event_id index to our cache keys
+ * since the DB_DataObject stuff doesn't understand compound keys
+ * except for the primary.
+ *
+ * @return array
+ */
+ function _allCacheKeys() {
+ $keys = parent::_allCacheKeys();
+ $keys[] = self::multicacheKey('RSVP', array('profile_id' => $this->profile_id,
+ 'event_id' => $this->event_id));
+ return $keys;
+ }
+
+ /**
+ * The One True Thingy that must be defined and declared.
+ */
+ public static function schemaDef()
+ {
+ return array(
+ 'description' => 'Plan to attend event',
+ 'fields' => array(
+ 'id' => array('type' => 'char',
+ 'length' => 36,
+ 'not null' => true,
+ 'description' => 'UUID'),
+ 'uri' => array('type' => 'varchar',
+ 'length' => 255,
+ 'not null' => true),
+ 'profile_id' => array('type' => 'int'),
+ 'event_id' => array('type' => 'char',
+ 'length' => 36,
+ 'not null' => true,
+ 'description' => 'UUID'),
+ 'result' => array('type' => 'tinyint',
+ 'description' => '1, 0, or null for three-state yes, no, maybe'),
+ 'created' => array('type' => 'datetime',
+ 'not null' => true),
+ ),
+ 'primary key' => array('id'),
+ 'unique keys' => array(
+ 'rsvp_uri_key' => array('uri'),
+ 'rsvp_profile_event_key' => array('profile_id', 'event_id'),
+ ),
+ 'foreign keys' => array('rsvp_event_id_key' => array('event', array('event_id' => 'id')),
+ 'rsvp_profile_id__key' => array('profile', array('profile_id' => 'id'))),
+ 'indexes' => array('rsvp_created_idx' => array('created')),
+ );
+ }
+
+ function saveNew($profile, $event, $result, $options=array())
+ {
+ if (array_key_exists('uri', $options)) {
+ $other = RSVP::staticGet('uri', $options['uri']);
+ if (!empty($other)) {
+ throw new ClientException(_('RSVP already exists.'));
+ }
+ }
+
+ $other = RSVP::pkeyGet(array('profile_id' => $profile->id,
+ 'event_id' => $event->id));
+
+ if (!empty($other)) {
+ throw new ClientException(_('RSVP already exists.'));
+ }
+
+ $rsvp = new RSVP();
+
+ $rsvp->id = UUID::gen();
+ $rsvp->profile_id = $profile->id;
+ $rsvp->event_id = $event->id;
+ $rsvp->result = self::codeFor($result);
+
+ if (array_key_exists('created', $options)) {
+ $rsvp->created = $options['created'];
+ } else {
+ $rsvp->created = common_sql_now();
+ }
+
+ if (array_key_exists('uri', $options)) {
+ $rsvp->uri = $options['uri'];
+ } else {
+ $rsvp->uri = common_local_url('showrsvp',
+ array('id' => $rsvp->id));
+ }
+
+ $rsvp->insert();
+
+ // XXX: come up with something sexier
+
+ $content = sprintf(_('RSVPed %s for an event.'),
+ ($result == RSVP::POSITIVE) ? _('positively') :
+ ($result == RSVP::NEGATIVE) ? _('negatively') : _('possibly'));
+
+ $rendered = $content;
+
+ $options = array_merge(array('object_type' => $result),
+ $options);
+
+ if (!array_key_exists('uri', $options)) {
+ $options['uri'] = $rsvp->uri;
+ }
+
+ $eventNotice = $event->getNotice();
+
+ if (!empty($eventNotice)) {
+ $options['reply_to'] = $eventNotice->id;
+ }
+
+ $saved = Notice::saveNew($profile->id,
+ $content,
+ array_key_exists('source', $options) ?
+ $options['source'] : 'web',
+ $options);
+
+ return $saved;
+ }
+
+ function codeFor($verb)
+ {
+ return ($verb == RSVP::POSITIVE) ? 1 :
+ ($verb == RSVP::NEGATIVE) ? 0 : null;
+ }
+
+ static function verbFor($code)
+ {
+ return ($code == 1) ? RSVP::POSITIVE :
+ ($code == 0) ? RSVP::NEGATIVE : null;
+ }
+
+ function getNotice()
+ {
+ $notice = Notice::staticGet('uri', $this->uri);
+ if (empty($notice)) {
+ throw new ServerException("RSVP {$this->id} does not correspond to a notice in the DB.");
+ }
+ return $notice;
+ }
+
+ static function fromNotice($notice)
+ {
+ return RSVP::staticGet('uri', $notice->uri);
+ }
+
+ static function forEvent($event)
+ {
+ $rsvps = array(RSVP::POSITIVE => array(), RSVP::NEGATIVE => array(), RSVP::POSSIBLE => array());
+
+ $rsvp = new RSVP();
+
+ $rsvp->event_id = $event->id;
+
+ if ($rsvp->find()) {
+ while ($rsvp->fetch()) {
+ $verb = self::verbFor($rsvp->result);
+ $rsvps[$verb][] = clone($rsvp);
+ }
+ }
+
+ return $rsvps;
+ }
+}
diff --git a/plugins/Event/cancelrsvp.php b/plugins/Event/cancelrsvp.php
new file mode 100644
index 0000000000..83dabe2de5
--- /dev/null
+++ b/plugins/Event/cancelrsvp.php
@@ -0,0 +1,207 @@
+.
+ *
+ * @category Event
+ * @package StatusNet
+ * @author Evan Prodromou
+ * @copyright 2011 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
+ * @link http://status.net/
+ */
+if (!defined('STATUSNET')) {
+ // This check helps protect against security problems;
+ // your code file can't be executed directly from the web.
+ exit(1);
+}
+
+/**
+ * RSVP for an event
+ *
+ * @category Event
+ * @package StatusNet
+ * @author Evan Prodromou
+ * @copyright 2011 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
+ * @link http://status.net/
+ */
+
+class CancelrsvpAction extends Action
+{
+ protected $user = null;
+ protected $rsvp = null;
+ protected $event = null;
+
+ /**
+ * Returns the title of the action
+ *
+ * @return string Action title
+ */
+
+ function title()
+ {
+ return _('Cancel RSVP');
+ }
+
+ /**
+ * For initializing members of the class.
+ *
+ * @param array $argarray misc. arguments
+ *
+ * @return boolean true
+ */
+
+ function prepare($argarray)
+ {
+ parent::prepare($argarray);
+ if ($this->boolean('ajax')) {
+ StatusNet::setApi(true); // short error results!
+ }
+
+ $rsvpId = $this->trimmed('rsvp');
+
+ if (empty($rsvpId)) {
+ throw new ClientException(_('No such rsvp.'));
+ }
+
+ $this->rsvp = RSVP::staticGet('id', $rsvpId);
+
+ if (empty($this->rsvp)) {
+ throw new ClientException(_('No such rsvp.'));
+ }
+
+ $this->event = Happening::staticGet('id', $this->rsvp->event_id);
+
+ if (empty($this->event)) {
+ throw new ClientException(_('No such event.'));
+ }
+
+ $this->user = common_current_user();
+
+ if (empty($this->user)) {
+ throw new ClientException(_('You must be logged in to RSVP for an event.'));
+ }
+
+ return true;
+ }
+
+ /**
+ * Handler method
+ *
+ * @param array $argarray is ignored since it's now passed in in prepare()
+ *
+ * @return void
+ */
+
+ function handle($argarray=null)
+ {
+ parent::handle($argarray);
+
+ if ($this->isPost()) {
+ $this->cancelRSVP();
+ } else {
+ $this->showPage();
+ }
+
+ return;
+ }
+
+ /**
+ * Add a new event
+ *
+ * @return void
+ */
+
+ function cancelRSVP()
+ {
+ try {
+ $notice = $this->rsvp->getNotice();
+ // NB: this will delete the rsvp, too
+ if (!empty($notice)) {
+ common_log(LOG_DEBUG, "Deleting notice...");
+ $notice->delete();
+ } else {
+ common_log(LOG_DEBUG, "Deleting RSVP alone...");
+ $this->rsvp->delete();
+ }
+ } catch (ClientException $ce) {
+ $this->error = $ce->getMessage();
+ $this->showPage();
+ return;
+ }
+
+ if ($this->boolean('ajax')) {
+ header('Content-Type: text/xml;charset=utf-8');
+ $this->xw->startDocument('1.0', 'UTF-8');
+ $this->elementStart('html');
+ $this->elementStart('head');
+ // TRANS: Page title after sending a notice.
+ $this->element('title', null, _('Event saved'));
+ $this->elementEnd('head');
+ $this->elementStart('body');
+ $this->elementStart('body');
+ $form = new RSVPForm($this->event, $this);
+ $form->show();
+ $this->elementEnd('body');
+ $this->elementEnd('body');
+ $this->elementEnd('html');
+ }
+ }
+
+ /**
+ * Show the event form
+ *
+ * @return void
+ */
+
+ function showContent()
+ {
+ if (!empty($this->error)) {
+ $this->element('p', 'error', $this->error);
+ }
+
+ $form = new CancelRSVPForm($this->rsvp, $this);
+
+ $form->show();
+
+ return;
+ }
+
+ /**
+ * Return true if read only.
+ *
+ * MAY override
+ *
+ * @param array $args other arguments
+ *
+ * @return boolean is read only action?
+ */
+
+ function isReadOnly($args)
+ {
+ if ($_SERVER['REQUEST_METHOD'] == 'GET' ||
+ $_SERVER['REQUEST_METHOD'] == 'HEAD') {
+ return true;
+ } else {
+ return false;
+ }
+ }
+}
diff --git a/plugins/Event/cancelrsvpform.php b/plugins/Event/cancelrsvpform.php
new file mode 100644
index 0000000000..8cccbdb661
--- /dev/null
+++ b/plugins/Event/cancelrsvpform.php
@@ -0,0 +1,128 @@
+.
+ *
+ * @category Event
+ * @package StatusNet
+ * @author Evan Prodromou
+ * @copyright 2011 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
+ * @link http://status.net/
+ */
+
+if (!defined('STATUSNET')) {
+ // This check helps protect against security problems;
+ // your code file can't be executed directly from the web.
+ exit(1);
+}
+
+/**
+ * A form to RSVP for an event
+ *
+ * @category General
+ * @package StatusNet
+ * @author Evan Prodromou
+ * @copyright 2011 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
+ * @link http://status.net/
+ */
+
+class CancelRSVPForm extends Form
+{
+ protected $rsvp = null;
+
+ function __construct($rsvp, $out=null)
+ {
+ parent::__construct($out);
+ $this->rsvp = $rsvp;
+ }
+
+ /**
+ * ID of the form
+ *
+ * @return int ID of the form
+ */
+
+ function id()
+ {
+ return 'form_event_rsvp';
+ }
+
+ /**
+ * class of the form
+ *
+ * @return string class of the form
+ */
+
+ function formClass()
+ {
+ return 'ajax';
+ }
+
+ /**
+ * Action of the form
+ *
+ * @return string URL of the action
+ */
+
+ function action()
+ {
+ return common_local_url('cancelrsvp');
+ }
+
+ /**
+ * Data elements of the form
+ *
+ * @return void
+ */
+
+ function formData()
+ {
+ $this->out->elementStart('fieldset', array('id' => 'new_rsvp_data'));
+
+ $this->out->hidden('rsvp', $this->rsvp->id);
+
+ switch (RSVP::verbFor($this->rsvp->result)) {
+ case RSVP::POSITIVE:
+ $this->out->text(_('You will attend this event.'));
+ break;
+ case RSVP::NEGATIVE:
+ $this->out->text(_('You will not attend this event.'));
+ break;
+ case RSVP::POSSIBLE:
+ $this->out->text(_('You might attend this event.'));
+ break;
+ }
+
+ $this->out->elementEnd('fieldset');
+ }
+
+ /**
+ * Action elements
+ *
+ * @return void
+ */
+
+ function formActions()
+ {
+ $this->out->submit('cancel', _m('BUTTON', 'Cancel'));
+ }
+}
diff --git a/plugins/Event/eventform.php b/plugins/Event/eventform.php
new file mode 100644
index 0000000000..e6bc1e7016
--- /dev/null
+++ b/plugins/Event/eventform.php
@@ -0,0 +1,164 @@
+.
+ *
+ * @category Event
+ * @package StatusNet
+ * @author Evan Prodromou
+ * @copyright 2011 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
+ * @link http://status.net/
+ */
+
+if (!defined('STATUSNET')) {
+ // This check helps protect against security problems;
+ // your code file can't be executed directly from the web.
+ exit(1);
+}
+
+/**
+ * Form for adding an event
+ *
+ * @category Event
+ * @package StatusNet
+ * @author Evan Prodromou
+ * @copyright 2011 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
+ * @link http://status.net/
+ */
+
+class EventForm extends Form
+{
+ /**
+ * ID of the form
+ *
+ * @return int ID of the form
+ */
+
+ function id()
+ {
+ return 'form_new_event';
+ }
+
+ /**
+ * class of the form
+ *
+ * @return string class of the form
+ */
+
+ function formClass()
+ {
+ return 'form_settings ajax-notice';
+ }
+
+ /**
+ * Action of the form
+ *
+ * @return string URL of the action
+ */
+
+ function action()
+ {
+ return common_local_url('newevent');
+ }
+
+ /**
+ * Data elements of the form
+ *
+ * @return void
+ */
+
+ function formData()
+ {
+ $this->out->elementStart('fieldset', array('id' => 'new_bookmark_data'));
+ $this->out->elementStart('ul', 'form_data');
+
+ $this->li();
+ $this->out->input('title',
+ _('Title'),
+ null,
+ _('Title of the event'));
+ $this->unli();
+
+ $this->li();
+ $this->out->input('startdate',
+ _('Start date'),
+ null,
+ _('Date the event starts'));
+ $this->unli();
+
+ $this->li();
+ $this->out->input('starttime',
+ _('Start time'),
+ null,
+ _('Time the event starts'));
+ $this->unli();
+
+ $this->li();
+ $this->out->input('enddate',
+ _('End date'),
+ null,
+ _('Date the event ends'));
+ $this->unli();
+
+ $this->li();
+ $this->out->input('endtime',
+ _('End time'),
+ null,
+ _('Time the event ends'));
+ $this->unli();
+
+ $this->li();
+ $this->out->input('location',
+ _('Location'),
+ null,
+ _('Event location'));
+ $this->unli();
+
+ $this->li();
+ $this->out->input('url',
+ _('URL'),
+ null,
+ _('URL for more information'));
+ $this->unli();
+
+ $this->li();
+ $this->out->input('description',
+ _('Description'),
+ null,
+ _('Description of the event'));
+ $this->unli();
+
+ $this->out->elementEnd('ul');
+ $this->out->elementEnd('fieldset');
+ }
+
+ /**
+ * Action elements
+ *
+ * @return void
+ */
+
+ function formActions()
+ {
+ $this->out->submit('submit', _m('BUTTON', 'Save'));
+ }
+}
diff --git a/plugins/Event/newevent.php b/plugins/Event/newevent.php
new file mode 100644
index 0000000000..0f5635487b
--- /dev/null
+++ b/plugins/Event/newevent.php
@@ -0,0 +1,241 @@
+.
+ *
+ * @category Event
+ * @package StatusNet
+ * @author Evan Prodromou
+ * @copyright 2011 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
+ * @link http://status.net/
+ */
+if (!defined('STATUSNET')) {
+ // This check helps protect against security problems;
+ // your code file can't be executed directly from the web.
+ exit(1);
+}
+
+/**
+ * Add a new event
+ *
+ * @category Event
+ * @package StatusNet
+ * @author Evan Prodromou
+ * @copyright 2011 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
+ * @link http://status.net/
+ */
+
+class NeweventAction extends Action
+{
+ protected $user = null;
+ protected $error = null;
+ protected $complete = null;
+ protected $title = null;
+ protected $location = null;
+ protected $description = null;
+ protected $start_time = null;
+ protected $end_time = null;
+
+ /**
+ * Returns the title of the action
+ *
+ * @return string Action title
+ */
+
+ function title()
+ {
+ return _('New event');
+ }
+
+ /**
+ * For initializing members of the class.
+ *
+ * @param array $argarray misc. arguments
+ *
+ * @return boolean true
+ */
+
+ function prepare($argarray)
+ {
+ parent::prepare($argarray);
+
+ $this->user = common_current_user();
+
+ if (empty($this->user)) {
+ throw new ClientException(_("Must be logged in to post a event."),
+ 403);
+ }
+
+ if ($this->isPost()) {
+ $this->checkSessionToken();
+ }
+
+ $this->title = $this->trimmed('title');
+ $this->location = $this->trimmed('location');
+ $this->url = $this->trimmed('url');
+ $this->description = $this->trimmed('description');
+
+ $start_date = $this->trimmed('start_date');
+ $start_time = $this->trimmed('start_time');
+ $end_date = $this->trimmed('end_date');
+ $end_time = $this->trimmed('end_time');
+
+ $this->start_time = strtotime($start_date . ' ' . $start_time);
+ $this->end_time = strtotime($end_date . ' ' . $end_time);
+
+ return true;
+ }
+
+ /**
+ * Handler method
+ *
+ * @param array $argarray is ignored since it's now passed in in prepare()
+ *
+ * @return void
+ */
+
+ function handle($argarray=null)
+ {
+ parent::handle($argarray);
+
+ if ($this->isPost()) {
+ $this->newEvent();
+ } else {
+ $this->showPage();
+ }
+
+ return;
+ }
+
+ /**
+ * Add a new event
+ *
+ * @return void
+ */
+
+ function newEvent()
+ {
+ try {
+ if (empty($this->title)) {
+ throw new ClientException(_('Event must have a title.'));
+ }
+
+ if (empty($this->start_time)) {
+ throw new ClientException(_('Event must have a start time.'));
+ }
+
+ if (empty($this->end_time)) {
+ throw new ClientException(_('Event must have an end time.'));
+ }
+
+ $profile = $this->user->getProfile();
+
+ $saved = Happening::saveNew($profile,
+ $this->start_time,
+ $this->end_time,
+ $this->title,
+ $this->location,
+ $this->description,
+ $this->url);
+
+ $event = Happening::fromNotice($saved);
+
+ RSVP::saveNew($profile, $event, RSVP::POSITIVE);
+
+ } catch (ClientException $ce) {
+ $this->error = $ce->getMessage();
+ $this->showPage();
+ return;
+ }
+
+ if ($this->boolean('ajax')) {
+ header('Content-Type: text/xml;charset=utf-8');
+ $this->xw->startDocument('1.0', 'UTF-8');
+ $this->elementStart('html');
+ $this->elementStart('head');
+ // TRANS: Page title after sending a notice.
+ $this->element('title', null, _('Event saved'));
+ $this->elementEnd('head');
+ $this->elementStart('body');
+ $this->showNotice($saved);
+ $this->elementEnd('body');
+ $this->elementEnd('html');
+ } else {
+ common_redirect($saved->bestUrl(), 303);
+ }
+ }
+
+ /**
+ * Show the event form
+ *
+ * @return void
+ */
+
+ function showContent()
+ {
+ if (!empty($this->error)) {
+ $this->element('p', 'error', $this->error);
+ }
+
+ $form = new EventForm($this);
+
+ $form->show();
+
+ return;
+ }
+
+ /**
+ * Return true if read only.
+ *
+ * MAY override
+ *
+ * @param array $args other arguments
+ *
+ * @return boolean is read only action?
+ */
+
+ function isReadOnly($args)
+ {
+ if ($_SERVER['REQUEST_METHOD'] == 'GET' ||
+ $_SERVER['REQUEST_METHOD'] == 'HEAD') {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+
+ /**
+ * Output a notice
+ *
+ * Used to generate the notice code for Ajax results.
+ *
+ * @param Notice $notice Notice that was saved
+ *
+ * @return void
+ */
+ function showNotice($notice)
+ {
+ $nli = new NoticeListItem($notice, $this);
+ $nli->show();
+ }
+}
diff --git a/plugins/Event/newrsvp.php b/plugins/Event/newrsvp.php
new file mode 100644
index 0000000000..4bacd129f4
--- /dev/null
+++ b/plugins/Event/newrsvp.php
@@ -0,0 +1,205 @@
+.
+ *
+ * @category Event
+ * @package StatusNet
+ * @author Evan Prodromou
+ * @copyright 2011 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
+ * @link http://status.net/
+ */
+if (!defined('STATUSNET')) {
+ // This check helps protect against security problems;
+ // your code file can't be executed directly from the web.
+ exit(1);
+}
+
+/**
+ * RSVP for an event
+ *
+ * @category Event
+ * @package StatusNet
+ * @author Evan Prodromou
+ * @copyright 2011 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
+ * @link http://status.net/
+ */
+
+class NewrsvpAction extends Action
+{
+ protected $user = null;
+ protected $event = null;
+ protected $type = null;
+
+ /**
+ * Returns the title of the action
+ *
+ * @return string Action title
+ */
+
+ function title()
+ {
+ return _('New RSVP');
+ }
+
+ /**
+ * For initializing members of the class.
+ *
+ * @param array $argarray misc. arguments
+ *
+ * @return boolean true
+ */
+
+ function prepare($argarray)
+ {
+ parent::prepare($argarray);
+ if ($this->boolean('ajax')) {
+ StatusNet::setApi(true); // short error results!
+ }
+
+ $eventId = $this->trimmed('event');
+
+ if (empty($eventId)) {
+ throw new ClientException(_('No such event.'));
+ }
+
+ $this->event = Happening::staticGet('id', $eventId);
+
+ if (empty($this->event)) {
+ throw new ClientException(_('No such event.'));
+ }
+
+ $this->user = common_current_user();
+
+ if (empty($this->user)) {
+ throw new ClientException(_('You must be logged in to RSVP for an event.'));
+ }
+
+ if ($this->arg('yes')) {
+ $this->type = RSVP::POSITIVE;
+ } else if ($this->arg('no')) {
+ $this->type = RSVP::NEGATIVE;
+ } else {
+ $this->type = RSVP::POSSIBLE;
+ }
+ return true;
+ }
+
+ /**
+ * Handler method
+ *
+ * @param array $argarray is ignored since it's now passed in in prepare()
+ *
+ * @return void
+ */
+
+ function handle($argarray=null)
+ {
+ parent::handle($argarray);
+
+ if ($this->isPost()) {
+ $this->newRSVP();
+ } else {
+ $this->showPage();
+ }
+
+ return;
+ }
+
+ /**
+ * Add a new event
+ *
+ * @return void
+ */
+
+ function newRSVP()
+ {
+ try {
+ $saved = RSVP::saveNew($this->user->getProfile(),
+ $this->event,
+ $this->type);
+ } catch (ClientException $ce) {
+ $this->error = $ce->getMessage();
+ $this->showPage();
+ return;
+ }
+
+ if ($this->boolean('ajax')) {
+ $rsvp = RSVP::fromNotice($saved);
+ header('Content-Type: text/xml;charset=utf-8');
+ $this->xw->startDocument('1.0', 'UTF-8');
+ $this->elementStart('html');
+ $this->elementStart('head');
+ // TRANS: Page title after sending a notice.
+ $this->element('title', null, _('Event saved'));
+ $this->elementEnd('head');
+ $this->elementStart('body');
+ $this->elementStart('body');
+ $cancel = new CancelRSVPForm($rsvp, $this);
+ $cancel->show();
+ $this->elementEnd('body');
+ $this->elementEnd('body');
+ $this->elementEnd('html');
+ } else {
+ common_redirect($saved->bestUrl(), 303);
+ }
+ }
+
+ /**
+ * Show the event form
+ *
+ * @return void
+ */
+
+ function showContent()
+ {
+ if (!empty($this->error)) {
+ $this->element('p', 'error', $this->error);
+ }
+
+ $form = new RSVPForm($this->event, $this);
+
+ $form->show();
+
+ return;
+ }
+
+ /**
+ * Return true if read only.
+ *
+ * MAY override
+ *
+ * @param array $args other arguments
+ *
+ * @return boolean is read only action?
+ */
+
+ function isReadOnly($args)
+ {
+ if ($_SERVER['REQUEST_METHOD'] == 'GET' ||
+ $_SERVER['REQUEST_METHOD'] == 'HEAD') {
+ return true;
+ } else {
+ return false;
+ }
+ }
+}
diff --git a/plugins/Event/rsvpform.php b/plugins/Event/rsvpform.php
new file mode 100644
index 0000000000..ad30f6a36e
--- /dev/null
+++ b/plugins/Event/rsvpform.php
@@ -0,0 +1,120 @@
+.
+ *
+ * @category Event
+ * @package StatusNet
+ * @author Evan Prodromou
+ * @copyright 2011 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
+ * @link http://status.net/
+ */
+
+if (!defined('STATUSNET')) {
+ // This check helps protect against security problems;
+ // your code file can't be executed directly from the web.
+ exit(1);
+}
+
+/**
+ * A form to RSVP for an event
+ *
+ * @category General
+ * @package StatusNet
+ * @author Evan Prodromou
+ * @copyright 2011 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
+ * @link http://status.net/
+ */
+
+class RSVPForm extends Form
+{
+ protected $event = null;
+
+ function __construct($event, $out=null)
+ {
+ parent::__construct($out);
+ $this->event = $event;
+ }
+
+ /**
+ * ID of the form
+ *
+ * @return int ID of the form
+ */
+
+ function id()
+ {
+ return 'form_event_rsvp';
+ }
+
+ /**
+ * class of the form
+ *
+ * @return string class of the form
+ */
+
+ function formClass()
+ {
+ return 'ajax';
+ }
+
+ /**
+ * Action of the form
+ *
+ * @return string URL of the action
+ */
+
+ function action()
+ {
+ return common_local_url('newrsvp');
+ }
+
+ /**
+ * Data elements of the form
+ *
+ * @return void
+ */
+
+ function formData()
+ {
+ $this->out->elementStart('fieldset', array('id' => 'new_rsvp_data'));
+
+ $this->out->text(_('RSVP: '));
+
+ $this->out->hidden('event', $this->event->id);
+
+ $this->out->elementEnd('fieldset');
+ }
+
+ /**
+ * Action elements
+ *
+ * @return void
+ */
+
+ function formActions()
+ {
+ $this->out->submit('yes', _m('BUTTON', 'Yes'));
+ $this->out->submit('no', _m('BUTTON', 'No'));
+ $this->out->submit('maybe', _m('BUTTON', 'Maybe'));
+ }
+}
diff --git a/plugins/Event/showevent.php b/plugins/Event/showevent.php
new file mode 100644
index 0000000000..7fb702f9db
--- /dev/null
+++ b/plugins/Event/showevent.php
@@ -0,0 +1,109 @@
+.
+ *
+ * @category Event
+ * @package StatusNet
+ * @author Evan Prodromou
+ * @copyright 2011 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
+ * @link http://status.net/
+ */
+
+if (!defined('STATUSNET')) {
+ // This check helps protect against security problems;
+ // your code file can't be executed directly from the web.
+ exit(1);
+}
+
+/**
+ * Show a single event, with associated information
+ *
+ * @category Event
+ * @package StatusNet
+ * @author Evan Prodromou
+ * @copyright 2011 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
+ * @link http://status.net/
+ */
+
+class ShoweventAction extends ShownoticeAction
+{
+ protected $id = null;
+ protected $event = null;
+
+ /**
+ * For initializing members of the class.
+ *
+ * @param array $argarray misc. arguments
+ *
+ * @return boolean true
+ */
+
+ function prepare($argarray)
+ {
+ OwnerDesignAction::prepare($argarray);
+
+ $this->id = $this->trimmed('id');
+
+ $this->event = Happening::staticGet('id', $this->id);
+
+ if (empty($this->event)) {
+ throw new ClientException(_('No such event.'), 404);
+ }
+
+ $this->notice = $this->event->getNotice();
+
+ if (empty($this->notice)) {
+ // Did we used to have it, and it got deleted?
+ throw new ClientException(_('No such event.'), 404);
+ }
+
+ $this->user = User::staticGet('id', $this->event->profile_id);
+
+ if (empty($this->user)) {
+ throw new ClientException(_('No such user.'), 404);
+ }
+
+ $this->profile = $this->user->getProfile();
+
+ if (empty($this->profile)) {
+ throw new ServerException(_('User without a profile.'));
+ }
+
+ $this->avatar = $this->profile->getAvatar(AVATAR_PROFILE_SIZE);
+
+ return true;
+ }
+
+ /**
+ * Title of the page
+ *
+ * Used by Action class for layout.
+ *
+ * @return string page tile
+ */
+
+ function title()
+ {
+ return $this->event->title;
+ }
+}
diff --git a/plugins/Event/showrsvp.php b/plugins/Event/showrsvp.php
new file mode 100644
index 0000000000..fde1d48f0e
--- /dev/null
+++ b/plugins/Event/showrsvp.php
@@ -0,0 +1,117 @@
+.
+ *
+ * @category RSVP
+ * @package StatusNet
+ * @author Evan Prodromou
+ * @copyright 2010 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
+ * @link http://status.net/
+ */
+
+if (!defined('STATUSNET')) {
+ // This check helps protect against security problems;
+ // your code file can't be executed directly from the web.
+ exit(1);
+}
+
+/**
+ * Show a single RSVP, with associated information
+ *
+ * @category RSVP
+ * @package StatusNet
+ * @author Evan Prodromou
+ * @copyright 2010 StatusNet, Inc.
+ * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
+ * @link http://status.net/
+ */
+
+class ShowrsvpAction extends ShownoticeAction
+{
+ protected $rsvp = null;
+ protected $event = null;
+
+ /**
+ * For initializing members of the class.
+ *
+ * @param array $argarray misc. arguments
+ *
+ * @return boolean true
+ */
+
+ function prepare($argarray)
+ {
+ OwnerDesignAction::prepare($argarray);
+
+ $this->id = $this->trimmed('id');
+
+ $this->rsvp = RSVP::staticGet('id', $this->id);
+
+ if (empty($this->rsvp)) {
+ throw new ClientException(_('No such RSVP.'), 404);
+ }
+
+ $this->event = $this->rsvp->getEvent();
+
+ if (empty($this->event)) {
+ throw new ClientException(_('No such Event.'), 404);
+ }
+
+ $this->notice = $this->rsvp->getNotice();
+
+ if (empty($this->notice)) {
+ // Did we used to have it, and it got deleted?
+ throw new ClientException(_('No such RSVP.'), 404);
+ }
+
+ $this->user = User::staticGet('id', $this->rsvp->profile_id);
+
+ if (empty($this->user)) {
+ throw new ClientException(_('No such user.'), 404);
+ }
+
+ $this->profile = $this->user->getProfile();
+
+ if (empty($this->profile)) {
+ throw new ServerException(_('User without a profile.'));
+ }
+
+ $this->avatar = $this->profile->getAvatar(AVATAR_PROFILE_SIZE);
+
+ return true;
+ }
+
+ /**
+ * Title of the page
+ *
+ * Used by Action class for layout.
+ *
+ * @return string page tile
+ */
+
+ function title()
+ {
+ return sprintf(_('%s\'s RSVP for "%s"'),
+ $this->user->nickname,
+ $this->event->title);
+ }
+}
diff --git a/plugins/LinkPreview/LinkPreviewPlugin.php b/plugins/LinkPreview/LinkPreviewPlugin.php
index 8bc726413d..2cc077d90e 100644
--- a/plugins/LinkPreview/LinkPreviewPlugin.php
+++ b/plugins/LinkPreview/LinkPreviewPlugin.php
@@ -51,7 +51,12 @@ class LinkPreviewPlugin extends Plugin
{
$user = common_current_user();
if ($user && common_config('attachments', 'process_links')) {
- $action->script($this->path('linkpreview.min.js'));
+ if (common_config('site', 'minify')) {
+ $js = 'linkpreview.min.js';
+ } else {
+ $js = 'linkpreview.js';
+ }
+ $action->script($this->path($js));
$data = json_encode(array(
'api' => common_local_url('oembedproxy'),
'width' => common_config('attachments', 'thumbwidth'),
diff --git a/plugins/LinkPreview/linkpreview.js b/plugins/LinkPreview/linkpreview.js
index 407934c5ae..132c4c8d77 100644
--- a/plugins/LinkPreview/linkpreview.js
+++ b/plugins/LinkPreview/linkpreview.js
@@ -74,174 +74,197 @@
}
};
- 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;
- },
-
- /**
- * 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) {
- var link = $(' ');
- link.find('a')
- .attr('href', url)
- .attr('target', '_blank')
- .last()
- .find('img')
- .attr('src', thumb)
- .attr('width', width)
- .attr('title', data.title || data.url || url);
- $('#' + id).empty();
- $('#' + id).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);
- }
- }
- },
-
- addPreviewArea: function(col) {
- var id = 'link-preview-' + col;
- $('#link-preview').append(' ');
- },
-
- clearLink: function(col) {
- var id = 'link-preview-' + col;
- $('#' + id).html('');
- },
-
- markLoading: function(col) {
- LinkPreview.state[col] = "loading";
- var id = 'link-preview-' + col;
- $('#' + id).attr('style', 'opacity: 0.5');
- },
-
- markDone: function(col) {
- LinkPreview.state[col] = "done";
- var id = 'link-preview-' + col;
- $('#' + id).removeAttr('style');
- },
-
- /**
- * Clear out any link preview data.
- */
- clear: function() {
- LinkPreview.links = [];
- $('#link-preview').empty();
- }
- };
-
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;
+ }
- $('#form_notice')
- .append('
')
+ // 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();
});
- // Piggyback on the counter update...
- var origCounter = SN.U.Counter;
- SN.U.Counter = function(form) {
- LinkPreview.previewLinks($('#notice_data-text').val());
- return origCounter(form);
- }
+ var LinkPreview = {
+ links: [],
+ state: [],
+ refresh: [],
+
+ /**
+ * Find URL links from the source text that may be interesting.
+ *
+ * @param {String} text
+ * @return {Array} list of URLs
+ */
+ findLinks: function (text)
+ {
+ // @fixme match this to core code
+ var re = /(?:^| )(https?:\/\/.+?\/.+?)(?= |$)/mg;
+ var links = [];
+ var matches;
+ while ((matches = re.exec(text)) !== null) {
+ links.push(matches[1]);
+ }
+ return links;
+ },
+
+ ensureArea: function() {
+ if (form.find('.link-preview').length < 1) {
+ form.append('
');
+ }
+ },
+
+ /**
+ * Start looking up info for a link preview...
+ * May start async data loads.
+ *
+ * @param {number} col: column number to insert preview into
+ */
+ prepLinkPreview: function(col)
+ {
+ var id = 'link-preview-' + col;
+ var url = LinkPreview.links[col];
+ LinkPreview.refresh[col] = false;
+ LinkPreview.markLoading(col);
+
+ oEmbed.lookup(url, function(data) {
+ var thumb = null;
+ var width = 100;
+ if (data && typeof data.thumbnail_url == "string") {
+ thumb = data.thumbnail_url;
+ if (typeof data.thumbnail_width !== "undefined") {
+ if (data.thumbnail_width < width) {
+ width = data.thumbnail_width;
+ }
+ }
+ } else if (data && data.type == 'photo' && typeof data.url == "string") {
+ thumb = data.url;
+ if (typeof data.width !== "undefined") {
+ if (data.width < width) {
+ width = data.width;
+ }
+ }
+ }
+
+ if (thumb) {
+ LinkPreview.ensureArea();
+ var link = $(' ');
+ link.find('a')
+ .attr('href', url)
+ .attr('target', '_blank')
+ .last()
+ .find('img')
+ .attr('src', thumb)
+ .attr('width', width)
+ .attr('title', data.title || data.url || url);
+ form.find('.' + id)
+ .empty()
+ .append(link);
+ } else {
+ // No thumbnail available or error retriving it.
+ LinkPreview.clearLink(col);
+ }
+
+ if (LinkPreview.refresh[col]) {
+ // Darn user has typed more characters.
+ // Go fetch another link!
+ LinkPreview.prepLinkPreview(col);
+ } else {
+ LinkPreview.markDone(col);
+ }
+ });
+ },
+
+ /**
+ * Update the live preview section with links found in the given text.
+ * May start async data loads.
+ *
+ * @param {String} text: free-form input text
+ */
+ previewLinks: function(text)
+ {
+ var i;
+ var old = LinkPreview.links;
+ var links = LinkPreview.findLinks(text);
+ LinkPreview.links = links;
+
+ // Check for existing common elements...
+ for (i = 0; i < old.length && i < links.length; i++) {
+ if (links[i] != old[i]) {
+ if (LinkPreview.state[i] == "loading") {
+ // Slate this column for a refresh when this one's done.
+ LinkPreview.refresh[i] = true;
+ } else {
+ // Change an existing entry!
+ LinkPreview.prepLinkPreview(i);
+ }
+ }
+ }
+ if (links.length > old.length) {
+ // Adding new entries, whee!
+ for (i = old.length; i < links.length; i++) {
+ LinkPreview.addPreviewArea(i);
+ LinkPreview.prepLinkPreview(i);
+ }
+ } else if (old.length > links.length) {
+ // Remove preview entries for links that have been removed.
+ for (i = links.length; i < old.length; i++) {
+ LinkPreview.clearLink(i);
+ }
+ }
+ if (links.length == 0) {
+ LinkPreview.clear();
+ }
+ },
+
+ addPreviewArea: function(col) {
+ LinkPreview.ensureArea();
+ var id = 'link-preview-' + col;
+ if (form.find('.' + id).length < 1) {
+ form.find('.link-preview').append(' ');
+ }
+ },
+
+ clearLink: function(col) {
+ var id = 'link-preview-' + col;
+ form.find('.' + id).html('');
+ },
+
+ markLoading: function(col) {
+ LinkPreview.state[col] = "loading";
+ var id = 'link-preview-' + col;
+ form.find('.' + id).attr('style', 'opacity: 0.5');
+ },
+
+ markDone: function(col) {
+ LinkPreview.state[col] = "done";
+ var id = 'link-preview-' + col;
+ form.find('.' + id).removeAttr('style');
+ },
+
+ /**
+ * Clear out any link preview data.
+ */
+ clear: function() {
+ LinkPreview.links = [];
+ form.find('.link-preview').remove();
+ }
+ };
+ form.data('LinkPreview', LinkPreview);
}
})();
diff --git a/plugins/LinkPreview/linkpreview.min.js b/plugins/LinkPreview/linkpreview.min.js
index a6fb9dba83..ea28f6bfee 100644
--- a/plugins/LinkPreview/linkpreview.min.js
+++ b/plugins/LinkPreview/linkpreview.min.js
@@ -1 +1 @@
-(function(){var a={api:"http://oohembed.com/oohembed",width:100,height:75,cache:{},callbacks:{},lookup:function(c,d){if(typeof a.cache[c]=="object"){d(a.cache[c])}else{if(typeof a.callbacks[c]=="undefined"){a.callbacks[c]=[d];a.rawLookup(c,function(g){a.cache[c]=g;var f=a.callbacks[c];a.callbacks[c]=undefined;for(var e=0;e ');h.find("a").attr("href",c).attr("target","_blank").last().find("img").attr("src",f).attr("width",g).attr("title",i.title||i.url||c);$("#"+e).empty();$("#"+e).append(h)}else{b.clearLink(d)}if(b.refresh[d]){b.prepLinkPreview(d)}else{b.markDone(d)}})},previewLinks:function(f){var e;var c=b.links;var d=b.findLinks(f);b.links=d;for(e=0;ec.length){for(e=c.length;ed.length){for(e=d.length;e')},clearLink:function(c){var d="link-preview-"+c;$("#"+d).html("")},markLoading:function(c){b.state[c]="loading";var d="link-preview-"+c;$("#"+d).attr("style","opacity: 0.5")},markDone:function(c){b.state[c]="done";var d="link-preview-"+c;$("#"+d).removeAttr("style")},clear:function(){b.links=[];$("#link-preview").empty()}};SN.Init.LinkPreview=function(c){if(c.api){a.api=c.api}if(c.width){a.width=c.width}if(c.height){a.height=c.height}$("#form_notice").append('
').bind("reset",function(){b.clear()});var d=SN.U.Counter;SN.U.Counter=function(e){b.previewLinks($("#notice_data-text").val());return d(e)}}})();
\ No newline at end of file
+(function(){var b={api:"http://oohembed.com/oohembed",width:100,height:75,cache:{},callbacks:{},lookup:function(d,e){if(typeof b.cache[d]=="object"){e(b.cache[d])}else{if(typeof b.callbacks[d]=="undefined"){b.callbacks[d]=[e];b.rawLookup(d,function(h){b.cache[d]=h;var g=b.callbacks[d];b.callbacks[d]=undefined;for(var f=0;f')}},prepLinkPreview:function(g){var h="link-preview-"+g;var f=e.links[g];e.refresh[g]=false;e.markLoading(g);b.lookup(f,function(l){var i=null;var j=100;if(l&&typeof l.thumbnail_url=="string"){i=l.thumbnail_url;if(typeof l.thumbnail_width!=="undefined"){if(l.thumbnail_width ');k.find("a").attr("href",f).attr("target","_blank").last().find("img").attr("src",i).attr("width",j).attr("title",l.title||l.url||f);d.find("."+h).empty().append(k)}else{e.clearLink(g)}if(e.refresh[g]){e.prepLinkPreview(g)}else{e.markDone(g)}})},previewLinks:function(j){var h;var f=e.links;var g=e.findLinks(j);e.links=g;for(h=0;hf.length){for(h=f.length;hg.length){for(h=g.length;h')}},clearLink:function(f){var g="link-preview-"+f;d.find("."+g).html("")},markLoading:function(f){e.state[f]="loading";var g="link-preview-"+f;d.find("."+g).attr("style","opacity: 0.5")},markDone:function(f){e.state[f]="done";var g="link-preview-"+f;d.find("."+g).removeAttr("style")},clear:function(){e.links=[];d.find(".link-preview").remove()}};d.data("LinkPreview",e)}})();
\ No newline at end of file
diff --git a/plugins/OpenID/openidlogin.php b/plugins/OpenID/openidlogin.php
index 8d25a2e9ac..850b68e63a 100644
--- a/plugins/OpenID/openidlogin.php
+++ b/plugins/OpenID/openidlogin.php
@@ -174,4 +174,8 @@ class OpenidloginAction extends Action
$nav = new LoginGroupNav($this);
$nav->show();
}
+
+ function showNoticeForm()
+ {
+ }
}
diff --git a/plugins/Poll/Poll.php b/plugins/Poll/Poll.php
index 60ec4399fd..65aad4830e 100644
--- a/plugins/Poll/Poll.php
+++ b/plugins/Poll/Poll.php
@@ -166,7 +166,9 @@ class Poll extends Managed_DataObject
$raw = array();
while ($pr->fetch()) {
- $raw[$pr->selection] = $pr->votes;
+ // Votes list 1-based
+ // Array stores 0-based
+ $raw[$pr->selection - 1] = $pr->votes;
}
$counts = array();
@@ -216,6 +218,7 @@ class Poll extends Managed_DataObject
array('id' => $p->id));
}
+ common_log(LOG_DEBUG, "Saving poll: $p->id $p->uri");
$p->insert();
$content = sprintf(_m('Poll: %s %s'),
diff --git a/plugins/Poll/newpoll.php b/plugins/Poll/newpoll.php
index 66386affa9..fa6eb798d7 100644
--- a/plugins/Poll/newpoll.php
+++ b/plugins/Poll/newpoll.php
@@ -127,6 +127,9 @@ class NewPollAction extends Action
function newPoll()
{
+ if ($this->boolean('ajax')) {
+ StatusNet::setApi(true);
+ }
try {
if (empty($this->question)) {
throw new ClientException(_('Poll must have a question.'));
@@ -147,7 +150,37 @@ class NewPollAction extends Action
return;
}
- common_redirect($saved->bestUrl(), 303);
+ if ($this->boolean('ajax')) {
+ header('Content-Type: text/xml;charset=utf-8');
+ $this->xw->startDocument('1.0', 'UTF-8');
+ $this->elementStart('html');
+ $this->elementStart('head');
+ // TRANS: Page title after sending a notice.
+ $this->element('title', null, _('Notice posted'));
+ $this->elementEnd('head');
+ $this->elementStart('body');
+ $this->showNotice($saved);
+ $this->elementEnd('body');
+ $this->elementEnd('html');
+ } else {
+ common_redirect($saved->bestUrl(), 303);
+ }
+ }
+
+ /**
+ * Output a notice
+ *
+ * Used to generate the notice code for Ajax results.
+ *
+ * @param Notice $notice Notice that was saved
+ *
+ * @return void
+ */
+ function showNotice($notice)
+ {
+ class_exists('NoticeList'); // @fixme hack for autoloader
+ $nli = new NoticeListItem($notice, $this);
+ $nli->show();
}
/**
@@ -163,7 +196,7 @@ class NewPollAction extends Action
}
$form = new NewPollForm($this,
- $this->questions,
+ $this->question,
$this->options);
$form->show();
diff --git a/plugins/Poll/newpollform.php b/plugins/Poll/newpollform.php
index fd5f28748b..73e516c891 100644
--- a/plugins/Poll/newpollform.php
+++ b/plugins/Poll/newpollform.php
@@ -83,7 +83,7 @@ class NewpollForm extends Form
function formClass()
{
- return 'form_settings';
+ return 'form_settings ajax-notice';
}
/**
diff --git a/plugins/Poll/poll.css b/plugins/Poll/poll.css
new file mode 100644
index 0000000000..5ba9c1588f
--- /dev/null
+++ b/plugins/Poll/poll.css
@@ -0,0 +1,10 @@
+.poll-block {
+ float: left;
+ height: 16px;
+ background: #8aa;
+ margin-right: 8px;
+}
+
+.poll-winner {
+ background: #4af;
+}
diff --git a/plugins/Poll/pollresultform.php b/plugins/Poll/pollresultform.php
index 1db86938fc..f4da10cb53 100644
--- a/plugins/Poll/pollresultform.php
+++ b/plugins/Poll/pollresultform.php
@@ -109,14 +109,33 @@ class PollResultForm extends Form
$out = $this->out;
$counts = $poll->countResponses();
- $out->element('p', 'poll-question', $poll->question);
- $out->elementStart('ul', 'poll-options');
- foreach ($poll->getOptions() as $i => $opt) {
- $out->elementStart('li');
- $out->text($counts[$i] . ' ' . $opt);
- $out->elementEnd('li');
+ $width = 200;
+ $max = max($counts);
+ if ($max == 0) {
+ $max = 1; // quick hack :D
}
- $out->elementEnd('ul');
+
+ $out->element('p', 'poll-question', $poll->question);
+ $out->elementStart('table', 'poll-results');
+ foreach ($poll->getOptions() as $i => $opt) {
+ $w = intval($counts[$i] * $width / $max) + 1;
+
+ $out->elementStart('tr');
+
+ $out->elementStart('td');
+ $out->text($opt);
+ $out->elementEnd('td');
+
+ $out->elementStart('td');
+ $out->element('span', array('class' => 'poll-block',
+ 'style' => "width: {$w}px"),
+ "\xc2\xa0"); // nbsp
+ $out->text($counts[$i]);
+ $out->elementEnd('td');
+
+ $out->elementEnd('tr');
+ }
+ $out->elementEnd('table');
}
/**
diff --git a/plugins/Poll/showpoll.php b/plugins/Poll/showpoll.php
index f5002701a2..21ac7647c0 100644
--- a/plugins/Poll/showpoll.php
+++ b/plugins/Poll/showpoll.php
@@ -108,4 +108,21 @@ class ShowPollAction extends ShownoticeAction
$this->poll->question);
}
+ /**
+ * @fixme combine the notice time with poll update time
+ */
+ function lastModified()
+ {
+ return Action::lastModified();
+ }
+
+
+ /**
+ * @fixme combine the notice time with poll update time
+ */
+ function etag()
+ {
+ return Action::etag();
+ }
+
}
diff --git a/theme/base/css/display.css b/theme/base/css/display.css
index 85ec1286b9..cd0e00d860 100644
--- a/theme/base/css/display.css
+++ b/theme/base/css/display.css
@@ -646,7 +646,8 @@ float:left;
max-width:322px;
}
.form_notice .error,
-.form_notice .success {
+.form_notice .success,
+.form_notice .notice-status {
float:left;
clear:both;
width:81.5%;
@@ -661,7 +662,8 @@ overflow:auto;
margin-right:2.5%;
font-size:1.1em;
}
-.form_notice .attach-status button.close {
+.form_notice .attach-status button.close,
+.form_notice .notice-status button.close,{
float:right;
font-size:0.8em;
}
diff --git a/theme/neo/css/display.css b/theme/neo/css/display.css
index 3d98b09f02..d230fc0a42 100644
--- a/theme/neo/css/display.css
+++ b/theme/neo/css/display.css
@@ -180,7 +180,8 @@ address {
}
.form_notice .error,
-.form_notice .success {
+.form_notice .success,
+.form_notice .notice-status {
width: 341px;
}
@@ -480,14 +481,14 @@ td.entity_profile { /* cf directory table */
margin-bottom: 10px;
}
-.error, .success {
+.error, .success, .notice-status {
background-color: #F7E8E8;
padding: 4px;
-webkit-border-radius: 6px;
-moz-border-radius: 6px;
border-radius: 6px;
}
-.success {
+.success, .notice-status {
background-color: #f2f2f2;
}
diff --git a/theme/rebase/css/display.css b/theme/rebase/css/display.css
index 810dd70bc1..c1ac1e8f4d 100644
--- a/theme/rebase/css/display.css
+++ b/theme/rebase/css/display.css
@@ -298,7 +298,8 @@ address .poweredby {
}
.form_notice .error,
-.form_notice .success {
+.form_notice .success,
+.form_notice .notice-status {
clear: left;
float: left;
overflow: auto;
@@ -319,7 +320,8 @@ address .poweredby {
padding: 6px 2px 6px 5px;
}
-.form_notice .attach-status button.close {
+.form_notice .attach-status button.close,
+.form_notice .notice-status button.close {
float:right;
font-size:0.8em;
}