From cbf16a4974564fc33e31efe686393bb2f007e03b Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Tue, 8 Mar 2011 15:10:30 -0800 Subject: [PATCH 01/29] Bookmark and poll plugins' custom notice forms now do AJAX submit, with the resulting notice appearing in the timeline. FormNoticeXHR now is triggered on any form labeled with class 'ajax-notice', so those other than the traditional notice form should work as long as they handle the AJAX submission and return a properly formatted notice. Things to watch out for: * to determine whether the resulting notice should show on the current timeline, the JS code needs to be able to check the author and such. Keeping the existing vcard bits helps for this! * the notice form submission stuff clears out inputs from your form -- test to make sure this behaves correctly * error messages returned from the thingy _should_ come through, but this needs more testing for consistency * while form components that aren't in a custom form should just be ignored, this should be tested more. (eg there's no location or attachment box for poll or bookmark plugins) * NoticeListItem isn't currently reachable via autoloader -- touch NoticeList explicitly before calling into it for now. --- js/util.js | 4 ++-- js/util.min.js | 2 +- lib/noticeform.php | 2 +- plugins/Bookmark/BookmarkPlugin.php | 7 ++++-- plugins/Bookmark/bookmarkform.php | 2 +- plugins/Bookmark/newbookmark.php | 35 ++++++++++++++++++++++++++- plugins/Poll/Poll.php | 1 + plugins/Poll/newpoll.php | 37 +++++++++++++++++++++++++++-- plugins/Poll/newpollform.php | 2 +- 9 files changed, 81 insertions(+), 11 deletions(-) diff --git a/js/util.js b/js/util.js index dcb8da4600..6bba34fae6 100644 --- a/js/util.js +++ b/js/util.js @@ -1263,7 +1263,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. @@ -1301,7 +1301,7 @@ 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); diff --git a/js/util.min.js b/js/util.min.js index 5e08cc78b5..88db1a1ac6 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("[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(".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.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/noticeform.php b/lib/noticeform.php index 271a37449c..e889036f55 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'; } /** 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/Poll/Poll.php b/plugins/Poll/Poll.php index 60ec4399fd..8d02f7de34 100644 --- a/plugins/Poll/Poll.php +++ b/plugins/Poll/Poll.php @@ -216,6 +216,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'; } /** From d990357be93dbe50b22c32e5fe41de4fbfc85c46 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Tue, 8 Mar 2011 15:44:51 -0800 Subject: [PATCH 02/29] fix off-by-one error in poll results display --- plugins/Poll/Poll.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/plugins/Poll/Poll.php b/plugins/Poll/Poll.php index 8d02f7de34..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(); From 921eb1eb23043d090655b94fa5a7cc7575b81815 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Tue, 8 Mar 2011 16:04:32 -0800 Subject: [PATCH 03/29] quickie bar chart for poll results --- plugins/Poll/poll.css | 10 ++++++++++ plugins/Poll/pollresultform.php | 33 ++++++++++++++++++++++++++------- 2 files changed, 36 insertions(+), 7 deletions(-) create mode 100644 plugins/Poll/poll.css 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'); } /** From 1413ed911f74c3cd2ba07f5e5c270375931620cc Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Tue, 8 Mar 2011 16:06:30 -0800 Subject: [PATCH 04/29] Hackaround for http caching problem on poll pages; the notice doesn't change, but the results do, so don't use the notice's last-modifeid time as a caching epoch. --- plugins/Poll/showpoll.php | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) 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(); + } + } From b8b1fbb6b5cb3a044acab3487e0a041220bf263d Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Tue, 8 Mar 2011 22:48:16 -0500 Subject: [PATCH 05/29] Give NoticeListItem its own class --- lib/noticelist.php | 589 -------------------------------------- lib/noticelistitem.php | 625 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 625 insertions(+), 589 deletions(-) create mode 100644 lib/noticelistitem.php 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)); + } + } +} From 24fd39d6ab2c0bf72ba0dc970883fdd9fa751465 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Tue, 8 Mar 2011 11:15:17 -0500 Subject: [PATCH 06/29] new Event plugin --- plugins/Event/Event.php | 184 ++++++++++++++++++++++ plugins/Event/EventPlugin.php | 284 ++++++++++++++++++++++++++++++++++ plugins/Event/RSVP.php | 184 ++++++++++++++++++++++ plugins/Event/newevent.php | 164 ++++++++++++++++++++ plugins/Event/newrsvp.php | 164 ++++++++++++++++++++ 5 files changed, 980 insertions(+) create mode 100644 plugins/Event/Event.php create mode 100644 plugins/Event/EventPlugin.php create mode 100644 plugins/Event/RSVP.php create mode 100644 plugins/Event/newevent.php create mode 100644 plugins/Event/newrsvp.php diff --git a/plugins/Event/Event.php b/plugins/Event/Event.php new file mode 100644 index 0000000000..38d68c91ed --- /dev/null +++ b/plugins/Event/Event.php @@ -0,0 +1,184 @@ + + * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3 + * @link http://status.net/ + * + * StatusNet - the distributed open-source microblogging tool + * Copyright (C) 2009, 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); +} + +require_once INSTALLDIR . '/classes/Memcached_DataObject.php'; + +/** + * Data class for counting greetings + * + * We use the DB_DataObject framework for data classes in StatusNet. Each + * table maps to a particular data class, making it easier to manipulate + * data. + * + * Data classes should extend Memcached_DataObject, the (slightly misnamed) + * extension of DB_DataObject that provides caching, internationalization, + * and other bits of good functionality to StatusNet-specific data classes. + * + * @category Action + * @package StatusNet + * @author Evan Prodromou + * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3 + * @link http://status.net/ + * + * @see DB_DataObject + */ + +class User_greeting_count extends Memcached_DataObject +{ + public $__table = 'user_greeting_count'; // table name + public $user_id; // int(4) primary_key not_null + public $greeting_count; // int(4) + + /** + * Get an instance by key + * + * This is a utility method to get a single instance with a given key value. + * + * @param string $k Key to use to lookup (usually 'user_id' for this class) + * @param mixed $v Value to lookup + * + * @return User_greeting_count object found, or null for no hits + * + */ + function staticGet($k, $v=null) + { + return Memcached_DataObject::staticGet('User_greeting_count', $k, $v); + } + + /** + * return table definition for DB_DataObject + * + * DB_DataObject needs to know something about the table to manipulate + * instances. This method provides all the DB_DataObject needs to know. + * + * @return array array of column definitions + */ + function table() + { + return array('user_id' => DB_DATAOBJECT_INT + DB_DATAOBJECT_NOTNULL, + 'greeting_count' => DB_DATAOBJECT_INT); + } + + /** + * return key definitions for DB_DataObject + * + * DB_DataObject needs to know about keys that the table has, since it + * won't appear in StatusNet's own keys list. In most cases, this will + * simply reference your keyTypes() function. + * + * @return array list of key field names + */ + function keys() + { + return array_keys($this->keyTypes()); + } + + /** + * return key definitions for Memcached_DataObject + * + * Our caching system uses the same key definitions, but uses a different + * method to get them. This key information is used to store and clear + * cached data, so be sure to list any key that will be used for static + * lookups. + * + * @return array associative array of key definitions, field name to type: + * 'K' for primary key: for compound keys, add an entry for each component; + * 'U' for unique keys: compound keys are not well supported here. + */ + function keyTypes() + { + return array('user_id' => 'K'); + } + + /** + * Magic formula for non-autoincrementing integer primary keys + * + * If a table has a single integer column as its primary key, DB_DataObject + * assumes that the column is auto-incrementing and makes a sequence table + * to do this incrementation. Since we don't need this for our class, we + * overload this method and return the magic formula that DB_DataObject needs. + * + * @return array magic three-false array that stops auto-incrementing. + */ + function sequenceKey() + { + return array(false, false, false); + } + + /** + * Increment a user's greeting count and return instance + * + * This method handles the ins and outs of creating a new greeting_count for a + * user or fetching the existing greeting count and incrementing its value. + * + * @param integer $user_id ID of the user to get a count for + * + * @return User_greeting_count instance for this user, with count already incremented. + */ + static function inc($user_id) + { + $gc = User_greeting_count::staticGet('user_id', $user_id); + + if (empty($gc)) { + + $gc = new User_greeting_count(); + + $gc->user_id = $user_id; + $gc->greeting_count = 1; + + $result = $gc->insert(); + + if (!$result) { + // TRANS: Exception thrown when the user greeting count could not be saved in the database. + // TRANS: %d is a user ID (number). + throw Exception(sprintf(_m("Could not save new greeting count for %d."), + $user_id)); + } + } else { + $orig = clone($gc); + + $gc->greeting_count++; + + $result = $gc->update($orig); + + if (!$result) { + // TRANS: Exception thrown when the user greeting count could not be saved in the database. + // TRANS: %d is a user ID (number). + throw Exception(sprintf(_m("Could not increment greeting count for %d."), + $user_id)); + } + } + + return $gc; + } +} diff --git a/plugins/Event/EventPlugin.php b/plugins/Event/EventPlugin.php new file mode 100644 index 0000000000..a0d1140f37 --- /dev/null +++ b/plugins/Event/EventPlugin.php @@ -0,0 +1,284 @@ +. + * + * @category Sample + * @package StatusNet + * @author Brion Vibber + * @author Evan Prodromou + * @copyright 2009 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); +} + +/** + * Sample plugin main class + * + * Each plugin requires a main class to interact with the StatusNet system. + * + * The main class usually extends the Plugin class that comes with StatusNet. + * + * The class has standard-named methods that will be called when certain events + * happen in the code base. These methods have names like 'onX' where X is an + * event name (see EVENTS.txt for the list of available events). Event handlers + * have pre-defined arguments, based on which event they're handling. A typical + * event handler: + * + * function onSomeEvent($paramA, &$paramB) + * { + * if ($paramA == 'jed') { + * throw new Exception(sprintf(_m("Invalid parameter %s"), $paramA)); + * } + * $paramB = 'spock'; + * return true; + * } + * + * Event handlers must return a boolean value. If they return false, all other + * event handlers for this event (in other plugins) will be skipped, and in some + * cases the default processing for that event would be skipped. This is great for + * replacing the default action of an event. + * + * If the handler returns true, processing of other event handlers and the default + * processing will continue. This is great for extending existing functionality. + * + * If the handler throws an exception, processing will stop, and the exception's + * error will be shown to the user. + * + * To install a plugin (like this one), site admins add the following code to + * their config.php file: + * + * addPlugin('Sample'); + * + * Plugins must be installed in one of the following directories: + * + * local/plugins/{$pluginclass}.php + * local/plugins/{$name}/{$pluginclass}.php + * local/{$pluginclass}.php + * local/{$name}/{$pluginclass}.php + * plugins/{$pluginclass}.php + * plugins/{$name}/{$pluginclass}.php + * + * Here, {$name} is the name of the plugin, like 'Sample', and {$pluginclass} is + * the name of the main class, like 'SamplePlugin'. Plugins that are part of the + * main StatusNet distribution go in 'plugins' and third-party or local ones go + * in 'local'. + * + * Simple plugins can be implemented as a single module. Others are more complex + * and require additional modules; these should use their own directory, like + * 'local/plugins/{$name}/'. All files related to the plugin, including images, + * JavaScript, CSS, external libraries or PHP modules should go in the plugin + * directory. + * + * @category Sample + * @package StatusNet + * @author Brion Vibber + * @author Evan Prodromou + * @copyright 2009 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ +class SamplePlugin extends Plugin +{ + /** + * Plugins are configured using public instance attributes. To set + * their values, site administrators use this syntax: + * + * addPlugin('Sample', array('attr1' => 'foo', 'attr2' => 'bar')); + * + * The same plugin class can be initialized multiple times with different + * arguments: + * + * addPlugin('EmailNotify', array('sendTo' => 'evan@status.net')); + * addPlugin('EmailNotify', array('sendTo' => 'brionv@status.net')); + * + */ + + public $attr1 = null; + public $attr2 = null; + + /** + * Initializer for this plugin + * + * Plugins overload this method to do any initialization they need, + * like connecting to remote servers or creating paths or so on. + * + * @return boolean hook value; true means continue processing, false means stop. + */ + function initialize() + { + return true; + } + + /** + * Cleanup for this plugin + * + * Plugins overload this method to do any cleanup they need, + * like disconnecting from remote servers or deleting temp files or so on. + * + * @return boolean hook value; true means continue processing, false means stop. + */ + function cleanup() + { + return true; + } + + /** + * Database schema setup + * + * Plugins can add their own tables to the StatusNet database. Plugins + * should use StatusNet's schema interface to add or delete tables. The + * ensureTable() method provides an easy way to ensure a table's structure + * and availability. + * + * By default, the schema is checked every time StatusNet is run (say, when + * a Web page is hit). Admins can configure their systems to only check the + * schema when the checkschema.php script is run, greatly improving performance. + * However, they need to remember to run that script after installing or + * upgrading a plugin! + * + * @see Schema + * @see ColumnDef + * + * @return boolean hook value; true means continue processing, false means stop. + */ + function onCheckSchema() + { + $schema = Schema::get(); + + // For storing user-submitted flags on profiles + + $schema->ensureTable('user_greeting_count', + array( + 'fields' => array( + 'user_id' => array('type' => 'int', 'not null' => true), + 'greeting_count' => array('type' => 'int'), + ), + 'primary key' => array('user_id'), + 'foreign keys' => array( + // Not all databases will support foreign keys, but even + // when not enforced it's helpful to include these definitions + // as documentation. + 'user_greeting_count_user_id_fkey' => array('user', array('user_id' => 'id')), + ), + ) + ); + + return true; + } + + /** + * Load related modules when needed + * + * Most non-trivial plugins will require extra modules to do their work. Typically + * these include data classes, action classes, widget classes, or external libraries. + * + * This method receives a class name and loads the PHP file related to that class. By + * tradition, action classes typically have files named for the action, all lower-case. + * Data classes are in files with the data class name, initial letter capitalized. + * + * Note that this method will be called for *all* overloaded classes, not just ones + * in this plugin! So, make sure to return true by default to let other plugins, and + * the core code, get a chance. + * + * @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 'HelloAction': + include_once $dir . '/' . strtolower(mb_substr($cls, 0, -6)) . '.php'; + return false; + case 'User_greeting_count': + include_once $dir . '/'.$cls.'.php'; + return false; + default: + return true; + } + } + + /** + * Map URLs to actions + * + * This event handler lets the plugin map URLs on the site to actions (and + * thus an action handler class). Note that the action handler class for an + * action will be named 'FoobarAction', where action = 'foobar'. The class + * must be loaded in the onAutoload() method. + * + * @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/hello', + array('action' => 'hello')); + return true; + } + + /** + * Modify the default menu to link to our custom action + * + * Using event handlers, it's possible to modify the default UI for pages + * almost without limit. In this method, we add a menu item to the default + * primary menu for the interface to link to our action. + * + * The Action class provides a rich set of events to hook, as well as output + * methods. + * + * @param Action $action The current action handler. Use this to + * do any output. + * + * @return boolean hook value; true means continue processing, false means stop. + * + * @see Action + */ + function onEndPrimaryNav($action) + { + // common_local_url() gets the correct URL for the action name + // we provide + + $action->menuItem(common_local_url('hello'), + _m('Hello'), _m('A warm greeting'), false, 'nav_hello'); + return true; + } + + function onPluginVersion(&$versions) + { + $versions[] = array('name' => 'Sample', + 'version' => STATUSNET_VERSION, + 'author' => 'Brion Vibber, Evan Prodromou', + 'homepage' => 'http://status.net/wiki/Plugin:Sample', + 'rawdescription' => + _m('A sample plugin to show basics of development for new hackers.')); + return true; + } +} diff --git a/plugins/Event/RSVP.php b/plugins/Event/RSVP.php new file mode 100644 index 0000000000..38d68c91ed --- /dev/null +++ b/plugins/Event/RSVP.php @@ -0,0 +1,184 @@ + + * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3 + * @link http://status.net/ + * + * StatusNet - the distributed open-source microblogging tool + * Copyright (C) 2009, 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); +} + +require_once INSTALLDIR . '/classes/Memcached_DataObject.php'; + +/** + * Data class for counting greetings + * + * We use the DB_DataObject framework for data classes in StatusNet. Each + * table maps to a particular data class, making it easier to manipulate + * data. + * + * Data classes should extend Memcached_DataObject, the (slightly misnamed) + * extension of DB_DataObject that provides caching, internationalization, + * and other bits of good functionality to StatusNet-specific data classes. + * + * @category Action + * @package StatusNet + * @author Evan Prodromou + * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3 + * @link http://status.net/ + * + * @see DB_DataObject + */ + +class User_greeting_count extends Memcached_DataObject +{ + public $__table = 'user_greeting_count'; // table name + public $user_id; // int(4) primary_key not_null + public $greeting_count; // int(4) + + /** + * Get an instance by key + * + * This is a utility method to get a single instance with a given key value. + * + * @param string $k Key to use to lookup (usually 'user_id' for this class) + * @param mixed $v Value to lookup + * + * @return User_greeting_count object found, or null for no hits + * + */ + function staticGet($k, $v=null) + { + return Memcached_DataObject::staticGet('User_greeting_count', $k, $v); + } + + /** + * return table definition for DB_DataObject + * + * DB_DataObject needs to know something about the table to manipulate + * instances. This method provides all the DB_DataObject needs to know. + * + * @return array array of column definitions + */ + function table() + { + return array('user_id' => DB_DATAOBJECT_INT + DB_DATAOBJECT_NOTNULL, + 'greeting_count' => DB_DATAOBJECT_INT); + } + + /** + * return key definitions for DB_DataObject + * + * DB_DataObject needs to know about keys that the table has, since it + * won't appear in StatusNet's own keys list. In most cases, this will + * simply reference your keyTypes() function. + * + * @return array list of key field names + */ + function keys() + { + return array_keys($this->keyTypes()); + } + + /** + * return key definitions for Memcached_DataObject + * + * Our caching system uses the same key definitions, but uses a different + * method to get them. This key information is used to store and clear + * cached data, so be sure to list any key that will be used for static + * lookups. + * + * @return array associative array of key definitions, field name to type: + * 'K' for primary key: for compound keys, add an entry for each component; + * 'U' for unique keys: compound keys are not well supported here. + */ + function keyTypes() + { + return array('user_id' => 'K'); + } + + /** + * Magic formula for non-autoincrementing integer primary keys + * + * If a table has a single integer column as its primary key, DB_DataObject + * assumes that the column is auto-incrementing and makes a sequence table + * to do this incrementation. Since we don't need this for our class, we + * overload this method and return the magic formula that DB_DataObject needs. + * + * @return array magic three-false array that stops auto-incrementing. + */ + function sequenceKey() + { + return array(false, false, false); + } + + /** + * Increment a user's greeting count and return instance + * + * This method handles the ins and outs of creating a new greeting_count for a + * user or fetching the existing greeting count and incrementing its value. + * + * @param integer $user_id ID of the user to get a count for + * + * @return User_greeting_count instance for this user, with count already incremented. + */ + static function inc($user_id) + { + $gc = User_greeting_count::staticGet('user_id', $user_id); + + if (empty($gc)) { + + $gc = new User_greeting_count(); + + $gc->user_id = $user_id; + $gc->greeting_count = 1; + + $result = $gc->insert(); + + if (!$result) { + // TRANS: Exception thrown when the user greeting count could not be saved in the database. + // TRANS: %d is a user ID (number). + throw Exception(sprintf(_m("Could not save new greeting count for %d."), + $user_id)); + } + } else { + $orig = clone($gc); + + $gc->greeting_count++; + + $result = $gc->update($orig); + + if (!$result) { + // TRANS: Exception thrown when the user greeting count could not be saved in the database. + // TRANS: %d is a user ID (number). + throw Exception(sprintf(_m("Could not increment greeting count for %d."), + $user_id)); + } + } + + return $gc; + } +} diff --git a/plugins/Event/newevent.php b/plugins/Event/newevent.php new file mode 100644 index 0000000000..a793ac6de2 --- /dev/null +++ b/plugins/Event/newevent.php @@ -0,0 +1,164 @@ + + * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3 + * @link http://status.net/ + * + * StatusNet - the distributed open-source microblogging tool + * Copyright (C) 2009, 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); +} + +/** + * Give a warm greeting to our friendly user + * + * This sample action shows some basic ways of doing output in an action + * class. + * + * Action classes have several output methods that they override from + * the parent class. + * + * @category Sample + * @package StatusNet + * @author Evan Prodromou + * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3 + * @link http://status.net/ + */ +class HelloAction extends Action +{ + var $user = null; + var $gc = null; + + /** + * Take arguments for running + * + * This method is called first, and it lets the action class get + * all its arguments and validate them. It's also the time + * to fetch any relevant data from the database. + * + * Action classes should run parent::prepare($args) as the first + * line of this method to make sure the default argument-processing + * happens. + * + * @param array $args $_REQUEST args + * + * @return boolean success flag + */ + function prepare($args) + { + parent::prepare($args); + + $this->user = common_current_user(); + + if (!empty($this->user)) { + $this->gc = User_greeting_count::inc($this->user->id); + } + + return true; + } + + /** + * Handle request + * + * This is the main method for handling a request. Note that + * most preparation should be done in the prepare() method; + * by the time handle() is called the action should be + * more or less ready to go. + * + * @param array $args $_REQUEST args; handled in prepare() + * + * @return void + */ + function handle($args) + { + parent::handle($args); + + $this->showPage(); + } + + /** + * Title of this page + * + * Override this method to show a custom title. + * + * @return string Title of the page + */ + function title() + { + if (empty($this->user)) { + return _m('Hello'); + } else { + return sprintf(_m('Hello, %s!'), $this->user->nickname); + } + } + + /** + * Show content in the content area + * + * The default StatusNet page has a lot of decorations: menus, + * logos, tabs, all that jazz. This method is used to show + * content in the content area of the page; it's the main + * thing you want to overload. + * + * This method also demonstrates use of a plural localized string. + * + * @return void + */ + function showContent() + { + if (empty($this->user)) { + $this->element('p', array('class' => 'greeting'), + _m('Hello, stranger!')); + } else { + $this->element('p', array('class' => 'greeting'), + sprintf(_m('Hello, %s'), $this->user->nickname)); + $this->element('p', array('class' => 'greeting_count'), + sprintf(_m('I have greeted you %d time.', + 'I have greeted you %d times.', + $this->gc->greeting_count), + $this->gc->greeting_count)); + } + } + + /** + * Return true if read only. + * + * Some actions only read from the database; others read and write. + * The simple database load-balancer built into StatusNet will + * direct read-only actions to database mirrors (if they are configured), + * and read-write actions to the master database. + * + * This defaults to false to avoid data integrity issues, but you + * should make sure to overload it for performance gains. + * + * @param array $args other arguments, if RO/RW status depends on them. + * + * @return boolean is read only action? + */ + function isReadOnly($args) + { + return false; + } +} diff --git a/plugins/Event/newrsvp.php b/plugins/Event/newrsvp.php new file mode 100644 index 0000000000..a793ac6de2 --- /dev/null +++ b/plugins/Event/newrsvp.php @@ -0,0 +1,164 @@ + + * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3 + * @link http://status.net/ + * + * StatusNet - the distributed open-source microblogging tool + * Copyright (C) 2009, 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); +} + +/** + * Give a warm greeting to our friendly user + * + * This sample action shows some basic ways of doing output in an action + * class. + * + * Action classes have several output methods that they override from + * the parent class. + * + * @category Sample + * @package StatusNet + * @author Evan Prodromou + * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3 + * @link http://status.net/ + */ +class HelloAction extends Action +{ + var $user = null; + var $gc = null; + + /** + * Take arguments for running + * + * This method is called first, and it lets the action class get + * all its arguments and validate them. It's also the time + * to fetch any relevant data from the database. + * + * Action classes should run parent::prepare($args) as the first + * line of this method to make sure the default argument-processing + * happens. + * + * @param array $args $_REQUEST args + * + * @return boolean success flag + */ + function prepare($args) + { + parent::prepare($args); + + $this->user = common_current_user(); + + if (!empty($this->user)) { + $this->gc = User_greeting_count::inc($this->user->id); + } + + return true; + } + + /** + * Handle request + * + * This is the main method for handling a request. Note that + * most preparation should be done in the prepare() method; + * by the time handle() is called the action should be + * more or less ready to go. + * + * @param array $args $_REQUEST args; handled in prepare() + * + * @return void + */ + function handle($args) + { + parent::handle($args); + + $this->showPage(); + } + + /** + * Title of this page + * + * Override this method to show a custom title. + * + * @return string Title of the page + */ + function title() + { + if (empty($this->user)) { + return _m('Hello'); + } else { + return sprintf(_m('Hello, %s!'), $this->user->nickname); + } + } + + /** + * Show content in the content area + * + * The default StatusNet page has a lot of decorations: menus, + * logos, tabs, all that jazz. This method is used to show + * content in the content area of the page; it's the main + * thing you want to overload. + * + * This method also demonstrates use of a plural localized string. + * + * @return void + */ + function showContent() + { + if (empty($this->user)) { + $this->element('p', array('class' => 'greeting'), + _m('Hello, stranger!')); + } else { + $this->element('p', array('class' => 'greeting'), + sprintf(_m('Hello, %s'), $this->user->nickname)); + $this->element('p', array('class' => 'greeting_count'), + sprintf(_m('I have greeted you %d time.', + 'I have greeted you %d times.', + $this->gc->greeting_count), + $this->gc->greeting_count)); + } + } + + /** + * Return true if read only. + * + * Some actions only read from the database; others read and write. + * The simple database load-balancer built into StatusNet will + * direct read-only actions to database mirrors (if they are configured), + * and read-write actions to the master database. + * + * This defaults to false to avoid data integrity issues, but you + * should make sure to overload it for performance gains. + * + * @param array $args other arguments, if RO/RW status depends on them. + * + * @return boolean is read only action? + */ + function isReadOnly($args) + { + return false; + } +} From 0b30fb3971ab1ac3a2bf9e4efa969a449490fb5c Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Tue, 8 Mar 2011 11:28:53 -0500 Subject: [PATCH 07/29] Make event-specific stuff in EventPlugin --- plugins/Event/EventPlugin.php | 212 ++++------------------------------ 1 file changed, 24 insertions(+), 188 deletions(-) diff --git a/plugins/Event/EventPlugin.php b/plugins/Event/EventPlugin.php index a0d1140f37..af0875c805 100644 --- a/plugins/Event/EventPlugin.php +++ b/plugins/Event/EventPlugin.php @@ -1,9 +1,9 @@ . * - * @category Sample + * @category Event * @package StatusNet - * @author Brion Vibber * @author Evan Prodromou - * @copyright 2009 StatusNet, Inc. + * @copyright 2011 StatusNet, Inc. * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 * @link http://status.net/ */ @@ -36,129 +35,19 @@ if (!defined('STATUSNET')) { } /** - * Sample plugin main class - * - * Each plugin requires a main class to interact with the StatusNet system. - * - * The main class usually extends the Plugin class that comes with StatusNet. - * - * The class has standard-named methods that will be called when certain events - * happen in the code base. These methods have names like 'onX' where X is an - * event name (see EVENTS.txt for the list of available events). Event handlers - * have pre-defined arguments, based on which event they're handling. A typical - * event handler: - * - * function onSomeEvent($paramA, &$paramB) - * { - * if ($paramA == 'jed') { - * throw new Exception(sprintf(_m("Invalid parameter %s"), $paramA)); - * } - * $paramB = 'spock'; - * return true; - * } - * - * Event handlers must return a boolean value. If they return false, all other - * event handlers for this event (in other plugins) will be skipped, and in some - * cases the default processing for that event would be skipped. This is great for - * replacing the default action of an event. - * - * If the handler returns true, processing of other event handlers and the default - * processing will continue. This is great for extending existing functionality. - * - * If the handler throws an exception, processing will stop, and the exception's - * error will be shown to the user. - * - * To install a plugin (like this one), site admins add the following code to - * their config.php file: - * - * addPlugin('Sample'); - * - * Plugins must be installed in one of the following directories: - * - * local/plugins/{$pluginclass}.php - * local/plugins/{$name}/{$pluginclass}.php - * local/{$pluginclass}.php - * local/{$name}/{$pluginclass}.php - * plugins/{$pluginclass}.php - * plugins/{$name}/{$pluginclass}.php - * - * Here, {$name} is the name of the plugin, like 'Sample', and {$pluginclass} is - * the name of the main class, like 'SamplePlugin'. Plugins that are part of the - * main StatusNet distribution go in 'plugins' and third-party or local ones go - * in 'local'. - * - * Simple plugins can be implemented as a single module. Others are more complex - * and require additional modules; these should use their own directory, like - * 'local/plugins/{$name}/'. All files related to the plugin, including images, - * JavaScript, CSS, external libraries or PHP modules should go in the plugin - * directory. + * Event plugin * * @category Sample * @package StatusNet - * @author Brion Vibber * @author Evan Prodromou - * @copyright 2009 StatusNet, Inc. + * @copyright 2011 StatusNet, Inc. * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 * @link http://status.net/ */ -class SamplePlugin extends Plugin +class EventPlugin extends MicroappPlugin { /** - * Plugins are configured using public instance attributes. To set - * their values, site administrators use this syntax: - * - * addPlugin('Sample', array('attr1' => 'foo', 'attr2' => 'bar')); - * - * The same plugin class can be initialized multiple times with different - * arguments: - * - * addPlugin('EmailNotify', array('sendTo' => 'evan@status.net')); - * addPlugin('EmailNotify', array('sendTo' => 'brionv@status.net')); - * - */ - - public $attr1 = null; - public $attr2 = null; - - /** - * Initializer for this plugin - * - * Plugins overload this method to do any initialization they need, - * like connecting to remote servers or creating paths or so on. - * - * @return boolean hook value; true means continue processing, false means stop. - */ - function initialize() - { - return true; - } - - /** - * Cleanup for this plugin - * - * Plugins overload this method to do any cleanup they need, - * like disconnecting from remote servers or deleting temp files or so on. - * - * @return boolean hook value; true means continue processing, false means stop. - */ - function cleanup() - { - return true; - } - - /** - * Database schema setup - * - * Plugins can add their own tables to the StatusNet database. Plugins - * should use StatusNet's schema interface to add or delete tables. The - * ensureTable() method provides an easy way to ensure a table's structure - * and availability. - * - * By default, the schema is checked every time StatusNet is run (say, when - * a Web page is hit). Admins can configure their systems to only check the - * schema when the checkschema.php script is run, greatly improving performance. - * However, they need to remember to run that script after installing or - * upgrading a plugin! + * Set up our tables (event and rsvp) * * @see Schema * @see ColumnDef @@ -169,23 +58,8 @@ class SamplePlugin extends Plugin { $schema = Schema::get(); - // For storing user-submitted flags on profiles - - $schema->ensureTable('user_greeting_count', - array( - 'fields' => array( - 'user_id' => array('type' => 'int', 'not null' => true), - 'greeting_count' => array('type' => 'int'), - ), - 'primary key' => array('user_id'), - 'foreign keys' => array( - // Not all databases will support foreign keys, but even - // when not enforced it's helpful to include these definitions - // as documentation. - 'user_greeting_count_user_id_fkey' => array('user', array('user_id' => 'id')), - ), - ) - ); + $schema->ensureTable('event', Event::schemaDef()); + $schema->ensureTable('rsvp', RSVP::schemaDef()); return true; } @@ -193,17 +67,6 @@ class SamplePlugin extends Plugin /** * Load related modules when needed * - * Most non-trivial plugins will require extra modules to do their work. Typically - * these include data classes, action classes, widget classes, or external libraries. - * - * This method receives a class name and loads the PHP file related to that class. By - * tradition, action classes typically have files named for the action, all lower-case. - * Data classes are in files with the data class name, initial letter capitalized. - * - * Note that this method will be called for *all* overloaded classes, not just ones - * in this plugin! So, make sure to return true by default to let other plugins, and - * the core code, get a chance. - * * @param string $cls Name of the class to be loaded * * @return boolean hook value; true means continue processing, false means stop. @@ -214,10 +77,12 @@ class SamplePlugin extends Plugin switch ($cls) { - case 'HelloAction': + case 'NeweventAction': + case 'NewrsvpAction': include_once $dir . '/' . strtolower(mb_substr($cls, 0, -6)) . '.php'; return false; - case 'User_greeting_count': + case 'Event': + case 'RSVP': include_once $dir . '/'.$cls.'.php'; return false; default: @@ -228,57 +93,28 @@ class SamplePlugin extends Plugin /** * Map URLs to actions * - * This event handler lets the plugin map URLs on the site to actions (and - * thus an action handler class). Note that the action handler class for an - * action will be named 'FoobarAction', where action = 'foobar'. The class - * must be loaded in the onAutoload() method. - * * @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/hello', - array('action' => 'hello')); - return true; - } - - /** - * Modify the default menu to link to our custom action - * - * Using event handlers, it's possible to modify the default UI for pages - * almost without limit. In this method, we add a menu item to the default - * primary menu for the interface to link to our action. - * - * The Action class provides a rich set of events to hook, as well as output - * methods. - * - * @param Action $action The current action handler. Use this to - * do any output. - * - * @return boolean hook value; true means continue processing, false means stop. - * - * @see Action - */ - function onEndPrimaryNav($action) - { - // common_local_url() gets the correct URL for the action name - // we provide - - $action->menuItem(common_local_url('hello'), - _m('Hello'), _m('A warm greeting'), false, 'nav_hello'); + $m->connect('main/event/new', + array('action' => 'newevent')); + $m->connect('main/event/rsvp', + array('action' => 'newrsvp')); return true; } function onPluginVersion(&$versions) { - $versions[] = array('name' => 'Sample', + $versions[] = array('name' => 'Event', 'version' => STATUSNET_VERSION, - 'author' => 'Brion Vibber, Evan Prodromou', - 'homepage' => 'http://status.net/wiki/Plugin:Sample', - 'rawdescription' => - _m('A sample plugin to show basics of development for new hackers.')); + 'author' => 'Evan Prodromou', + 'homepage' => 'http://status.net/wiki/Plugin:Event', + 'description' => + _m('Event invitations and RSVPs.')); return true; } } From f715821cf0a4f95eb0d265eda62abed294474b2b Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Wed, 9 Mar 2011 02:33:26 -0500 Subject: [PATCH 08/29] Kinda complete and kinda working-ish events --- plugins/Event/Event.php | 184 ------------------------ plugins/Event/EventPlugin.php | 224 ++++++++++++++++++++++++++++- plugins/Event/Happening.php | 198 ++++++++++++++++++++++++++ plugins/Event/RSVP.php | 255 ++++++++++++++++++---------------- plugins/Event/eventform.php | 157 +++++++++++++++++++++ plugins/Event/newevent.php | 217 ++++++++++++++++------------- plugins/Event/showevent.php | 109 +++++++++++++++ plugins/Event/showrsvp.php | 117 ++++++++++++++++ 8 files changed, 1060 insertions(+), 401 deletions(-) delete mode 100644 plugins/Event/Event.php create mode 100644 plugins/Event/Happening.php create mode 100644 plugins/Event/eventform.php create mode 100644 plugins/Event/showevent.php create mode 100644 plugins/Event/showrsvp.php diff --git a/plugins/Event/Event.php b/plugins/Event/Event.php deleted file mode 100644 index 38d68c91ed..0000000000 --- a/plugins/Event/Event.php +++ /dev/null @@ -1,184 +0,0 @@ - - * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3 - * @link http://status.net/ - * - * StatusNet - the distributed open-source microblogging tool - * Copyright (C) 2009, 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); -} - -require_once INSTALLDIR . '/classes/Memcached_DataObject.php'; - -/** - * Data class for counting greetings - * - * We use the DB_DataObject framework for data classes in StatusNet. Each - * table maps to a particular data class, making it easier to manipulate - * data. - * - * Data classes should extend Memcached_DataObject, the (slightly misnamed) - * extension of DB_DataObject that provides caching, internationalization, - * and other bits of good functionality to StatusNet-specific data classes. - * - * @category Action - * @package StatusNet - * @author Evan Prodromou - * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3 - * @link http://status.net/ - * - * @see DB_DataObject - */ - -class User_greeting_count extends Memcached_DataObject -{ - public $__table = 'user_greeting_count'; // table name - public $user_id; // int(4) primary_key not_null - public $greeting_count; // int(4) - - /** - * Get an instance by key - * - * This is a utility method to get a single instance with a given key value. - * - * @param string $k Key to use to lookup (usually 'user_id' for this class) - * @param mixed $v Value to lookup - * - * @return User_greeting_count object found, or null for no hits - * - */ - function staticGet($k, $v=null) - { - return Memcached_DataObject::staticGet('User_greeting_count', $k, $v); - } - - /** - * return table definition for DB_DataObject - * - * DB_DataObject needs to know something about the table to manipulate - * instances. This method provides all the DB_DataObject needs to know. - * - * @return array array of column definitions - */ - function table() - { - return array('user_id' => DB_DATAOBJECT_INT + DB_DATAOBJECT_NOTNULL, - 'greeting_count' => DB_DATAOBJECT_INT); - } - - /** - * return key definitions for DB_DataObject - * - * DB_DataObject needs to know about keys that the table has, since it - * won't appear in StatusNet's own keys list. In most cases, this will - * simply reference your keyTypes() function. - * - * @return array list of key field names - */ - function keys() - { - return array_keys($this->keyTypes()); - } - - /** - * return key definitions for Memcached_DataObject - * - * Our caching system uses the same key definitions, but uses a different - * method to get them. This key information is used to store and clear - * cached data, so be sure to list any key that will be used for static - * lookups. - * - * @return array associative array of key definitions, field name to type: - * 'K' for primary key: for compound keys, add an entry for each component; - * 'U' for unique keys: compound keys are not well supported here. - */ - function keyTypes() - { - return array('user_id' => 'K'); - } - - /** - * Magic formula for non-autoincrementing integer primary keys - * - * If a table has a single integer column as its primary key, DB_DataObject - * assumes that the column is auto-incrementing and makes a sequence table - * to do this incrementation. Since we don't need this for our class, we - * overload this method and return the magic formula that DB_DataObject needs. - * - * @return array magic three-false array that stops auto-incrementing. - */ - function sequenceKey() - { - return array(false, false, false); - } - - /** - * Increment a user's greeting count and return instance - * - * This method handles the ins and outs of creating a new greeting_count for a - * user or fetching the existing greeting count and incrementing its value. - * - * @param integer $user_id ID of the user to get a count for - * - * @return User_greeting_count instance for this user, with count already incremented. - */ - static function inc($user_id) - { - $gc = User_greeting_count::staticGet('user_id', $user_id); - - if (empty($gc)) { - - $gc = new User_greeting_count(); - - $gc->user_id = $user_id; - $gc->greeting_count = 1; - - $result = $gc->insert(); - - if (!$result) { - // TRANS: Exception thrown when the user greeting count could not be saved in the database. - // TRANS: %d is a user ID (number). - throw Exception(sprintf(_m("Could not save new greeting count for %d."), - $user_id)); - } - } else { - $orig = clone($gc); - - $gc->greeting_count++; - - $result = $gc->update($orig); - - if (!$result) { - // TRANS: Exception thrown when the user greeting count could not be saved in the database. - // TRANS: %d is a user ID (number). - throw Exception(sprintf(_m("Could not increment greeting count for %d."), - $user_id)); - } - } - - return $gc; - } -} diff --git a/plugins/Event/EventPlugin.php b/plugins/Event/EventPlugin.php index af0875c805..d6d7e00fc1 100644 --- a/plugins/Event/EventPlugin.php +++ b/plugins/Event/EventPlugin.php @@ -58,7 +58,7 @@ class EventPlugin extends MicroappPlugin { $schema = Schema::get(); - $schema->ensureTable('event', Event::schemaDef()); + $schema->ensureTable('event', Happening::schemaDef()); $schema->ensureTable('rsvp', RSVP::schemaDef()); return true; @@ -79,9 +79,14 @@ class EventPlugin extends MicroappPlugin { case 'NeweventAction': case 'NewrsvpAction': + case 'ShoweventAction': + case 'ShowrsvpAction': include_once $dir . '/' . strtolower(mb_substr($cls, 0, -6)) . '.php'; return false; - case 'Event': + case 'EventForm': + include_once $dir . '/'.strtolower($cls).'.php'; + break; + case 'Happening': case 'RSVP': include_once $dir . '/'.$cls.'.php'; return false; @@ -104,6 +109,12 @@ class EventPlugin extends MicroappPlugin array('action' => 'newevent')); $m->connect('main/event/rsvp', array('action' => 'newrsvp')); + $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; } @@ -117,4 +128,213 @@ class EventPlugin extends MicroappPlugin _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; + } + } + + function showRSVPNotice($notice, $out) + { + $out->element('span', null, 'RSVP'); + return; + } + + function showEventNotice($notice, $out) + { + $out->raw($notice->rendered); + return; + } + + /** + * 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: + $happening = Happening::fromNotice($notice); + $happening->delete(); + break; + case RSVP::POSITIVE: + case RSVP::NEGATIVE: + case RSVP::POSSIBLE: + $rsvp = RSVP::fromNotice($notice); + $rsvp->delete(); + break; + } + } } diff --git a/plugins/Event/Happening.php b/plugins/Event/Happening.php new file mode 100644 index 0000000000..054e57c732 --- /dev/null +++ b/plugins/Event/Happening.php @@ -0,0 +1,198 @@ + + * @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 $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'), + 'start_time' => array('type' => 'datetime'), + 'end_time' => array('type' => 'datetime'), + 'title' => array('type' => 'varchar', + 'length' => 255, + 'not null' => true), + 'location' => array('type' => 'varchar', + 'length' => 255, + 'not null' => true), + 'description' => array('type' => 'text'), + 'created' => array('type' => 'datetime', + 'not null' => true), + ), + 'primary key' => array('id'), + 'unique keys' => array( + 'happening_uri_key' => array('uri'), + ), + ); + } + + function saveNew($profile, $start_time, $end_time, $title, $location, $description, $options) + { + 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; + + 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; + } + + $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() + { + return Happening::staticGet('uri', $notice->uri); + } +} diff --git a/plugins/Event/RSVP.php b/plugins/Event/RSVP.php index 38d68c91ed..69cae4b7dc 100644 --- a/plugins/Event/RSVP.php +++ b/plugins/Event/RSVP.php @@ -1,6 +1,6 @@ * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3 * @link http://status.net/ * - * @see DB_DataObject + * @see Managed_DataObject */ -class User_greeting_count extends Memcached_DataObject +class RSVP extends Managed_DataObject { - public $__table = 'user_greeting_count'; // table name - public $user_id; // int(4) primary_key not_null - public $greeting_count; // int(4) + 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 * - * This is a utility method to get a single instance with a given key value. - * - * @param string $k Key to use to lookup (usually 'user_id' for this class) + * @param string $k Key to use to lookup (usually 'id' for this class) * @param mixed $v Value to lookup * - * @return User_greeting_count object found, or null for no hits + * @return RSVP object found, or null for no hits * */ function staticGet($k, $v=null) { - return Memcached_DataObject::staticGet('User_greeting_count', $k, $v); + return Memcached_DataObject::staticGet('RSVP', $k, $v); } /** - * return table definition for DB_DataObject - * - * DB_DataObject needs to know something about the table to manipulate - * instances. This method provides all the DB_DataObject needs to know. - * - * @return array array of column definitions + * The One True Thingy that must be defined and declared. */ - function table() + public static function schemaDef() { - return array('user_id' => DB_DATAOBJECT_INT + DB_DATAOBJECT_NOTNULL, - 'greeting_count' => DB_DATAOBJECT_INT); + return array( + 'description' => 'A real-world 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')), + ); } - /** - * return key definitions for DB_DataObject - * - * DB_DataObject needs to know about keys that the table has, since it - * won't appear in StatusNet's own keys list. In most cases, this will - * simply reference your keyTypes() function. - * - * @return array list of key field names - */ - function keys() + function saveNew($profile, $event, $result, $options) { - return array_keys($this->keyTypes()); - } - - /** - * return key definitions for Memcached_DataObject - * - * Our caching system uses the same key definitions, but uses a different - * method to get them. This key information is used to store and clear - * cached data, so be sure to list any key that will be used for static - * lookups. - * - * @return array associative array of key definitions, field name to type: - * 'K' for primary key: for compound keys, add an entry for each component; - * 'U' for unique keys: compound keys are not well supported here. - */ - function keyTypes() - { - return array('user_id' => 'K'); - } - - /** - * Magic formula for non-autoincrementing integer primary keys - * - * If a table has a single integer column as its primary key, DB_DataObject - * assumes that the column is auto-incrementing and makes a sequence table - * to do this incrementation. Since we don't need this for our class, we - * overload this method and return the magic formula that DB_DataObject needs. - * - * @return array magic three-false array that stops auto-incrementing. - */ - function sequenceKey() - { - return array(false, false, false); - } - - /** - * Increment a user's greeting count and return instance - * - * This method handles the ins and outs of creating a new greeting_count for a - * user or fetching the existing greeting count and incrementing its value. - * - * @param integer $user_id ID of the user to get a count for - * - * @return User_greeting_count instance for this user, with count already incremented. - */ - static function inc($user_id) - { - $gc = User_greeting_count::staticGet('user_id', $user_id); - - if (empty($gc)) { - - $gc = new User_greeting_count(); - - $gc->user_id = $user_id; - $gc->greeting_count = 1; - - $result = $gc->insert(); - - if (!$result) { - // TRANS: Exception thrown when the user greeting count could not be saved in the database. - // TRANS: %d is a user ID (number). - throw Exception(sprintf(_m("Could not save new greeting count for %d."), - $user_id)); - } - } else { - $orig = clone($gc); - - $gc->greeting_count++; - - $result = $gc->update($orig); - - if (!$result) { - // TRANS: Exception thrown when the user greeting count could not be saved in the database. - // TRANS: %d is a user ID (number). - throw Exception(sprintf(_m("Could not increment greeting count for %d."), - $user_id)); + if (array_key_exists('uri', $options)) { + $other = RSVP::staticGet('uri', $options['uri']); + if (!empty($other)) { + throw new ClientException(_('RSVP already exists.')); } } - return $gc; + $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; + } + + function verbFor($code) + { + return ($code == 1) ? RSVP::POSITIVE : + ($code == 0) ? RSVP::NEGATIVE : null; + } + + function getNotice() + { + return Notice::staticGet('uri', $this->uri); + } + + static function fromNotice() + { + return RSVP::staticGet('uri', $notice->uri); } } diff --git a/plugins/Event/eventform.php b/plugins/Event/eventform.php new file mode 100644 index 0000000000..8347639b5b --- /dev/null +++ b/plugins/Event/eventform.php @@ -0,0 +1,157 @@ +. + * + * @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'; + } + + /** + * 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('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 index a793ac6de2..66b15ea41b 100644 --- a/plugins/Event/newevent.php +++ b/plugins/Event/newevent.php @@ -1,17 +1,11 @@ - * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3 - * @link http://status.net/ - * * StatusNet - the distributed open-source microblogging tool - * Copyright (C) 2009, StatusNet, Inc. + * Copyright (C) 2011, StatusNet, Inc. + * + * Add a new event + * + * PHP version 5 * * 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 @@ -25,140 +19,173 @@ * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . + * + * @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); } /** - * Give a warm greeting to our friendly user + * Add a new event * - * This sample action shows some basic ways of doing output in an action - * class. - * - * Action classes have several output methods that they override from - * the parent class. - * - * @category Sample - * @package StatusNet - * @author Evan Prodromou - * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3 - * @link http://status.net/ + * @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 HelloAction extends Action + +class NeweventAction extends Action { - var $user = null; - var $gc = null; + 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; /** - * Take arguments for running + * Returns the title of the action * - * This method is called first, and it lets the action class get - * all its arguments and validate them. It's also the time - * to fetch any relevant data from the database. - * - * Action classes should run parent::prepare($args) as the first - * line of this method to make sure the default argument-processing - * happens. - * - * @param array $args $_REQUEST args - * - * @return boolean success flag + * @return string Action title */ - function prepare($args) + + function title() { - parent::prepare($args); + 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)) { - $this->gc = User_greeting_count::inc($this->user->id); + 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->description = $this->trimmed('description'); + return true; } /** - * Handle request + * Handler method * - * This is the main method for handling a request. Note that - * most preparation should be done in the prepare() method; - * by the time handle() is called the action should be - * more or less ready to go. - * - * @param array $args $_REQUEST args; handled in prepare() + * @param array $argarray is ignored since it's now passed in in prepare() * * @return void */ - function handle($args) - { - parent::handle($args); - $this->showPage(); - } - - /** - * Title of this page - * - * Override this method to show a custom title. - * - * @return string Title of the page - */ - function title() + function handle($argarray=null) { - if (empty($this->user)) { - return _m('Hello'); + parent::handle($argarray); + + if ($this->isPost()) { + $this->newEvent(); } else { - return sprintf(_m('Hello, %s!'), $this->user->nickname); + $this->showPage(); } + + return; } /** - * Show content in the content area - * - * The default StatusNet page has a lot of decorations: menus, - * logos, tabs, all that jazz. This method is used to show - * content in the content area of the page; it's the main - * thing you want to overload. - * - * This method also demonstrates use of a plural localized string. + * Add a new event * * @return void */ + + function newEvent() + { + try { + if (empty($this->title)) { + throw new ClientException(_('Event must have a title.')); + } + + if (empty($this->url)) { + throw new ClientException(_('Event must have an URL.')); + } + + + $saved = Event::saveNew($this->user->getProfile(), + $this->title, + $this->url, + $this->tags, + $this->description); + + } catch (ClientException $ce) { + $this->error = $ce->getMessage(); + $this->showPage(); + return; + } + + common_redirect($saved->bestUrl(), 303); + } + + /** + * Show the event form + * + * @return void + */ + function showContent() { - if (empty($this->user)) { - $this->element('p', array('class' => 'greeting'), - _m('Hello, stranger!')); - } else { - $this->element('p', array('class' => 'greeting'), - sprintf(_m('Hello, %s'), $this->user->nickname)); - $this->element('p', array('class' => 'greeting_count'), - sprintf(_m('I have greeted you %d time.', - 'I have greeted you %d times.', - $this->gc->greeting_count), - $this->gc->greeting_count)); + if (!empty($this->error)) { + $this->element('p', 'error', $this->error); } + + $form = new EventForm($this); + + $form->show(); + + return; } /** * Return true if read only. * - * Some actions only read from the database; others read and write. - * The simple database load-balancer built into StatusNet will - * direct read-only actions to database mirrors (if they are configured), - * and read-write actions to the master database. + * MAY override * - * This defaults to false to avoid data integrity issues, but you - * should make sure to overload it for performance gains. - * - * @param array $args other arguments, if RO/RW status depends on them. + * @param array $args other arguments * * @return boolean is read only action? */ + function isReadOnly($args) { - return false; + if ($_SERVER['REQUEST_METHOD'] == 'GET' || + $_SERVER['REQUEST_METHOD'] == 'HEAD') { + return true; + } else { + return false; + } } } diff --git a/plugins/Event/showevent.php b/plugins/Event/showevent.php new file mode 100644 index 0000000000..f8b032c111 --- /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 = Event::staticGet('id', $this->id); + + if (empty($this->event)) { + throw new ClientException(_('No such event.'), 404); + } + + $this->notice = $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); + } +} From ca36dfecf9217670f31d9c1c65c337064cd2413a Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Wed, 9 Mar 2011 02:37:51 -0500 Subject: [PATCH 09/29] more fixes for events --- plugins/Event/Happening.php | 2 +- plugins/Event/newevent.php | 26 +++++++++++++++++++------- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/plugins/Event/Happening.php b/plugins/Event/Happening.php index 054e57c732..5ede26706b 100644 --- a/plugins/Event/Happening.php +++ b/plugins/Event/Happening.php @@ -112,7 +112,7 @@ class Happening extends Managed_DataObject ); } - function saveNew($profile, $start_time, $end_time, $title, $location, $description, $options) + function saveNew($profile, $start_time, $end_time, $title, $location, $description, $options=array()) { if (array_key_exists('uri', $options)) { $other = Happening::staticGet('uri', $options['uri']); diff --git a/plugins/Event/newevent.php b/plugins/Event/newevent.php index 66b15ea41b..fe23e8825d 100644 --- a/plugins/Event/newevent.php +++ b/plugins/Event/newevent.php @@ -93,6 +93,14 @@ class NeweventAction extends Action $this->location = $this->trimmed('location'); $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; } @@ -130,16 +138,20 @@ class NeweventAction extends Action throw new ClientException(_('Event must have a title.')); } - if (empty($this->url)) { - throw new ClientException(_('Event must have an URL.')); + 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.')); + } $saved = Event::saveNew($this->user->getProfile(), - $this->title, - $this->url, - $this->tags, - $this->description); + $this->start_time, + $this->end_time, + $this->title, + $this->location, + $this->description); } catch (ClientException $ce) { $this->error = $ce->getMessage(); @@ -147,7 +159,7 @@ class NeweventAction extends Action return; } - common_redirect($saved->bestUrl(), 303); + } /** From 53e67b5ed5bb01e19fef02b98932e5d9d6e76160 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Wed, 9 Mar 2011 02:48:14 -0500 Subject: [PATCH 10/29] make new event work, sort of --- plugins/Event/EventPlugin.php | 2 +- plugins/Event/eventform.php | 2 +- plugins/Event/newevent.php | 44 +++++++++++++++++++++++++++++------ 3 files changed, 39 insertions(+), 9 deletions(-) diff --git a/plugins/Event/EventPlugin.php b/plugins/Event/EventPlugin.php index d6d7e00fc1..f33de09d78 100644 --- a/plugins/Event/EventPlugin.php +++ b/plugins/Event/EventPlugin.php @@ -58,7 +58,7 @@ class EventPlugin extends MicroappPlugin { $schema = Schema::get(); - $schema->ensureTable('event', Happening::schemaDef()); + $schema->ensureTable('happening', Happening::schemaDef()); $schema->ensureTable('rsvp', RSVP::schemaDef()); return true; diff --git a/plugins/Event/eventform.php b/plugins/Event/eventform.php index 8347639b5b..8d108020a6 100644 --- a/plugins/Event/eventform.php +++ b/plugins/Event/eventform.php @@ -66,7 +66,7 @@ class EventForm extends Form function formClass() { - return 'form_settings ajax'; + return 'form_settings ajax-notice'; } /** diff --git a/plugins/Event/newevent.php b/plugins/Event/newevent.php index fe23e8825d..365e9c1434 100644 --- a/plugins/Event/newevent.php +++ b/plugins/Event/newevent.php @@ -146,12 +146,12 @@ class NeweventAction extends Action throw new ClientException(_('Event must have an end time.')); } - $saved = Event::saveNew($this->user->getProfile(), - $this->start_time, - $this->end_time, - $this->title, - $this->location, - $this->description); + $saved = Happening::saveNew($this->user->getProfile(), + $this->start_time, + $this->end_time, + $this->title, + $this->location, + $this->description); } catch (ClientException $ce) { $this->error = $ce->getMessage(); @@ -159,7 +159,21 @@ class NeweventAction extends Action 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); + } } /** @@ -200,4 +214,20 @@ class NeweventAction extends Action 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(); + } } From f00f5f20b8b373fc8631071e8be6522a937a7690 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Wed, 9 Mar 2011 10:07:30 -0500 Subject: [PATCH 11/29] add url to events --- plugins/Event/EventPlugin.php | 65 +++++++++++++++++++++++++++++++++-- plugins/Event/Happening.php | 24 +++++++++---- plugins/Event/RSVP.php | 2 +- plugins/Event/eventform.php | 7 ++++ plugins/Event/newevent.php | 8 +++-- 5 files changed, 94 insertions(+), 12 deletions(-) diff --git a/plugins/Event/EventPlugin.php b/plugins/Event/EventPlugin.php index f33de09d78..896ecaf5c8 100644 --- a/plugins/Event/EventPlugin.php +++ b/plugins/Event/EventPlugin.php @@ -300,8 +300,69 @@ class EventPlugin extends MicroappPlugin function showEventNotice($notice, $out) { - $out->raw($notice->rendered); - return; + $profile = $notice->getProfile(); + $event = Happening::fromNotice($notice); + + assert(!empty($event)); + assert(!empty($profile)); + + $out->elementStart('div', 'vevent'); + + $out->elementStart('h3'); + + if (!empty($event->url)) { + $out->element('a', + array('href' => $att->url, + 'class' => 'event-title entry-title summary'), + $event->title); + } else { + $out->text($event->title); + } + + $out->elementEnd('h3'); + + $out->elementStart('div', 'event-times'); + $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'); + + if (!empty($event->description)) { + $out->element('div', 'description', $event->description); + } + + if (!empty($event->location)) { + $out->element('div', 'location', $event->location); + } + + $out->elementStart('div', array('class' => 'event-info entry-content')); + + $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'); + + $out->elementEnd('div'); } /** diff --git a/plugins/Event/Happening.php b/plugins/Event/Happening.php index 5ede26706b..503cd8af14 100644 --- a/plugins/Event/Happening.php +++ b/plugins/Event/Happening.php @@ -60,6 +60,7 @@ class Happening extends Managed_DataObject public $end_time; // datetime public $title; // varchar(255) public $location; // varchar(255) + public $url; // varchar(255) public $description; // text public $created; // datetime @@ -92,15 +93,16 @@ class Happening extends Managed_DataObject 'uri' => array('type' => 'varchar', 'length' => 255, 'not null' => true), - 'profile_id' => array('type' => 'int'), - 'start_time' => array('type' => 'datetime'), - 'end_time' => array('type' => 'datetime'), + '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, - 'not null' => true), + 'length' => 255), + 'url' => array('type' => 'varchar', + 'length' => 255), 'description' => array('type' => 'text'), 'created' => array('type' => 'datetime', 'not null' => true), @@ -109,10 +111,13 @@ class Happening extends Managed_DataObject '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, $options=array()) + 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']); @@ -130,6 +135,7 @@ class Happening extends Managed_DataObject $ev->title = $title; $ev->location = $location; $ev->description = $description; + $ev->url = $url; if (array_key_exists('created', $options)) { $ev->created = $options['created']; @@ -177,6 +183,10 @@ class Happening extends Managed_DataObject $options['uri'] = $ev->uri; } + if (!empty($url)) { + $options['urls'] = array($url); + } + $saved = Notice::saveNew($profile->id, $content, array_key_exists('source', $options) ? @@ -191,7 +201,7 @@ class Happening extends Managed_DataObject return Notice::staticGet('uri', $this->uri); } - static function fromNotice() + static function fromNotice($notice) { return Happening::staticGet('uri', $notice->uri); } diff --git a/plugins/Event/RSVP.php b/plugins/Event/RSVP.php index 69cae4b7dc..851978e819 100644 --- a/plugins/Event/RSVP.php +++ b/plugins/Event/RSVP.php @@ -192,7 +192,7 @@ class RSVP extends Managed_DataObject return Notice::staticGet('uri', $this->uri); } - static function fromNotice() + static function fromNotice($notice) { return RSVP::staticGet('uri', $notice->uri); } diff --git a/plugins/Event/eventform.php b/plugins/Event/eventform.php index 8d108020a6..e6bc1e7016 100644 --- a/plugins/Event/eventform.php +++ b/plugins/Event/eventform.php @@ -133,6 +133,13 @@ class EventForm extends Form _('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'), diff --git a/plugins/Event/newevent.php b/plugins/Event/newevent.php index 365e9c1434..7c0fd0177f 100644 --- a/plugins/Event/newevent.php +++ b/plugins/Event/newevent.php @@ -91,6 +91,7 @@ class NeweventAction extends Action $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'); @@ -146,12 +147,15 @@ class NeweventAction extends Action throw new ClientException(_('Event must have an end time.')); } - $saved = Happening::saveNew($this->user->getProfile(), + $profile = $this->user->getProfile(); + + $saved = Happening::saveNew($profile, $this->start_time, $this->end_time, $this->title, $this->location, - $this->description); + $this->description, + $this->url); } catch (ClientException $ce) { $this->error = $ce->getMessage(); From a4b3edaf40a397fca66f1f6be4b3c21af03ab6c8 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Wed, 9 Mar 2011 10:11:59 -0500 Subject: [PATCH 12/29] Login actions don't show the notice form --- actions/login.php | 4 ++++ actions/register.php | 4 ++++ plugins/OpenID/openidlogin.php | 4 ++++ 3 files changed, 12 insertions(+) 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/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/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() + { + } } From 3d310c80bed74015d0a56d3f6e37d23302b7a633 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Wed, 9 Mar 2011 10:12:53 -0500 Subject: [PATCH 13/29] no notice form on error pages --- lib/error.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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() + { + } } From b150b9439a05d954e8ea17bc366b50d2a50e4ae8 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Wed, 9 Mar 2011 10:15:19 -0500 Subject: [PATCH 14/29] don't show register in login if already logged in --- lib/logingroupnav.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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'), From 04a3157681ed5375220c1030b7d5043413ecd65a Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Wed, 9 Mar 2011 10:25:50 -0500 Subject: [PATCH 15/29] stream for direct responses --- classes/Notice.php | 47 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/classes/Notice.php b/classes/Notice.php index d520f4728f..d49a8f483b 100644 --- a/classes/Notice.php +++ b/classes/Notice.php @@ -463,6 +463,10 @@ class Notice extends Memcached_DataObject // was not the root of the conversation. What to do now? self::blow('notice:conversation_ids:%d', $this->conversation); + + if (!empty($this->reply_to)) { + self::blow('notice:responses:%d', $this->reply_to); + } if (!empty($this->repeat_of)) { self::blow('notice:repeats:%d', $this->repeat_of); @@ -492,6 +496,7 @@ class Notice extends Memcached_DataObject $this->blowOnInsert(); self::blow('profile:notice_ids:%d;last', $this->profile_id); + self::blow('notice:responses:%d', $this->id); if ($this->isPublic()) { self::blow('public;last'); @@ -2133,4 +2138,46 @@ class Notice extends Memcached_DataObject ($this->is_local != Notice::GATEWAY)); } } + + function responseStream($offset=0, $limit=20, $since_id=0, $max_id=0) + { + $ids = Notice::stream(array($this, '_responseStreamDirect'), + array(), + 'notice:responses:'.$id, + $offset, $limit, $since_id, $max_id); + + return Notice::getStreamByIds($ids); + } + + function _responseStreamDirect($offset=0, $limit=20, $since_id=0, $max_id=0) + { + $notice = new Notice(); + + $notice->selectAdd(); // clears it + $notice->selectAdd('id'); + + $notice->reply_to = $this->reply_to; + + $notice->orderBy('created DESC, id DESC'); + + if (!is_null($offset)) { + $notice->limit($offset, $limit); + } + + Notice::addWhereSinceId($notice, $since_id); + Notice::addWhereMaxId($notice, $max_id); + + $ids = array(); + + if ($notice->find()) { + while ($notice->fetch()) { + $ids[] = $notice->id; + } + } + + $notice->free(); + $notice = NULL; + + return $ids; + } } From 1e4e9a8456907821a2c84ae17ec4f28b0fb44604 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Wed, 9 Mar 2011 10:40:19 -0500 Subject: [PATCH 16/29] Revert "stream for direct responses" I don't actually need this so I'm reverting it. This reverts commit 04a3157681ed5375220c1030b7d5043413ecd65a. --- classes/Notice.php | 47 ---------------------------------------------- 1 file changed, 47 deletions(-) diff --git a/classes/Notice.php b/classes/Notice.php index d49a8f483b..d520f4728f 100644 --- a/classes/Notice.php +++ b/classes/Notice.php @@ -463,10 +463,6 @@ class Notice extends Memcached_DataObject // was not the root of the conversation. What to do now? self::blow('notice:conversation_ids:%d', $this->conversation); - - if (!empty($this->reply_to)) { - self::blow('notice:responses:%d', $this->reply_to); - } if (!empty($this->repeat_of)) { self::blow('notice:repeats:%d', $this->repeat_of); @@ -496,7 +492,6 @@ class Notice extends Memcached_DataObject $this->blowOnInsert(); self::blow('profile:notice_ids:%d;last', $this->profile_id); - self::blow('notice:responses:%d', $this->id); if ($this->isPublic()) { self::blow('public;last'); @@ -2138,46 +2133,4 @@ class Notice extends Memcached_DataObject ($this->is_local != Notice::GATEWAY)); } } - - function responseStream($offset=0, $limit=20, $since_id=0, $max_id=0) - { - $ids = Notice::stream(array($this, '_responseStreamDirect'), - array(), - 'notice:responses:'.$id, - $offset, $limit, $since_id, $max_id); - - return Notice::getStreamByIds($ids); - } - - function _responseStreamDirect($offset=0, $limit=20, $since_id=0, $max_id=0) - { - $notice = new Notice(); - - $notice->selectAdd(); // clears it - $notice->selectAdd('id'); - - $notice->reply_to = $this->reply_to; - - $notice->orderBy('created DESC, id DESC'); - - if (!is_null($offset)) { - $notice->limit($offset, $limit); - } - - Notice::addWhereSinceId($notice, $since_id); - Notice::addWhereMaxId($notice, $max_id); - - $ids = array(); - - if ($notice->find()) { - while ($notice->fetch()) { - $ids[] = $notice->id; - } - } - - $notice->free(); - $notice = NULL; - - return $ids; - } } From 728869e3110d3246dc1943a5d6ca14e88a2cc821 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Wed, 9 Mar 2011 10:40:49 -0500 Subject: [PATCH 17/29] show RSVPs for an event --- plugins/Event/EventPlugin.php | 10 ++++++++++ plugins/Event/Happening.php | 5 +++++ plugins/Event/RSVP.php | 18 ++++++++++++++++++ 3 files changed, 33 insertions(+) diff --git a/plugins/Event/EventPlugin.php b/plugins/Event/EventPlugin.php index 896ecaf5c8..d8d9b572ed 100644 --- a/plugins/Event/EventPlugin.php +++ b/plugins/Event/EventPlugin.php @@ -321,6 +321,8 @@ class EventPlugin extends MicroappPlugin $out->elementEnd('h3'); + // FIXME: better dates + $out->elementStart('div', 'event-times'); $out->element('abbr', array('class' => 'dtstart', 'title' => common_date_iso8601($event->start_time)), @@ -339,6 +341,14 @@ class EventPlugin extends MicroappPlugin $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]))); + $out->elementStart('div', array('class' => 'event-info entry-content')); $avatar = $profile->getAvatar(AVATAR_MINI_SIZE); diff --git a/plugins/Event/Happening.php b/plugins/Event/Happening.php index 503cd8af14..b2adb4d9b5 100644 --- a/plugins/Event/Happening.php +++ b/plugins/Event/Happening.php @@ -205,4 +205,9 @@ class Happening extends Managed_DataObject { return Happening::staticGet('uri', $notice->uri); } + + function getRSVPs() + { + return RSVP::forEvent($this); + } } diff --git a/plugins/Event/RSVP.php b/plugins/Event/RSVP.php index 851978e819..36c6b32ec7 100644 --- a/plugins/Event/RSVP.php +++ b/plugins/Event/RSVP.php @@ -196,4 +196,22 @@ class RSVP extends Managed_DataObject { 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 = $this->verbFor($rsvp->code); + $rsvps[$verb][] = clone($rsvp); + } + } + + return $rsvps; + } } From 35429c28e5bdde71fe9dff9e69ef795a31a96e8d Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Wed, 9 Mar 2011 12:28:25 -0500 Subject: [PATCH 18/29] updates to make RSVPs work --- plugins/Event/EventPlugin.php | 23 +++- plugins/Event/Happening.php | 6 + plugins/Event/RSVP.php | 32 ++++- plugins/Event/cancelrsvp.php | 202 ++++++++++++++++++++++++++++ plugins/Event/cancelrsvpform.php | 128 ++++++++++++++++++ plugins/Event/newevent.php | 4 + plugins/Event/newrsvp.php | 218 ++++++++++++++++++------------- plugins/Event/rsvpform.php | 120 +++++++++++++++++ plugins/Event/showevent.php | 4 +- 9 files changed, 638 insertions(+), 99 deletions(-) create mode 100644 plugins/Event/cancelrsvp.php create mode 100644 plugins/Event/cancelrsvpform.php create mode 100644 plugins/Event/rsvpform.php diff --git a/plugins/Event/EventPlugin.php b/plugins/Event/EventPlugin.php index d8d9b572ed..7ca2fa9c0e 100644 --- a/plugins/Event/EventPlugin.php +++ b/plugins/Event/EventPlugin.php @@ -79,11 +79,14 @@ class EventPlugin extends MicroappPlugin { 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': @@ -109,6 +112,8 @@ class EventPlugin extends MicroappPlugin 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}')); @@ -294,7 +299,7 @@ class EventPlugin extends MicroappPlugin function showRSVPNotice($notice, $out) { - $out->element('span', null, 'RSVP'); + $out->raw($notice->rendered); return; } @@ -312,7 +317,7 @@ class EventPlugin extends MicroappPlugin if (!empty($event->url)) { $out->element('a', - array('href' => $att->url, + array('href' => $event->url, 'class' => 'event-title entry-title summary'), $event->title); } else { @@ -349,6 +354,20 @@ class EventPlugin extends MicroappPlugin count($rsvps[RSVP::NEGATIVE]), count($rsvps[RSVP::POSSIBLE]))); + $user = common_current_user(); + + if (!empty($user)) { + $rsvp = $event->getRSVP($user->getProfile()); + + if (empty($rsvp)) { + $form = new RSVPForm($event, $out); + } else { + $form = new CancelRSVPForm($rsvp, $out); + } + + $form->show(); + } + $out->elementStart('div', array('class' => 'event-info entry-content')); $avatar = $profile->getAvatar(AVATAR_MINI_SIZE); diff --git a/plugins/Event/Happening.php b/plugins/Event/Happening.php index b2adb4d9b5..1a6a028dca 100644 --- a/plugins/Event/Happening.php +++ b/plugins/Event/Happening.php @@ -210,4 +210,10 @@ class Happening extends Managed_DataObject { return RSVP::forEvent($this); } + + function getRSVP($profile) + { + return RSVP::pkeyGet(array('profile_id' => $profile->id, + 'event_id' => $this->id)); + } } diff --git a/plugins/Event/RSVP.php b/plugins/Event/RSVP.php index 36c6b32ec7..22bd239a68 100644 --- a/plugins/Event/RSVP.php +++ b/plugins/Event/RSVP.php @@ -71,13 +71,27 @@ class RSVP extends Managed_DataObject 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); + } + /** * The One True Thingy that must be defined and declared. */ public static function schemaDef() { return array( - 'description' => 'A real-world event', + 'description' => 'Plan to attend event', 'fields' => array( 'id' => array('type' => 'char', 'length' => 36, @@ -107,7 +121,7 @@ class RSVP extends Managed_DataObject ); } - function saveNew($profile, $event, $result, $options) + function saveNew($profile, $event, $result, $options=array()) { if (array_key_exists('uri', $options)) { $other = RSVP::staticGet('uri', $options['uri']); @@ -181,7 +195,7 @@ class RSVP extends Managed_DataObject ($verb == RSVP::NEGATIVE) ? 0 : null; } - function verbFor($code) + static function verbFor($code) { return ($code == 1) ? RSVP::POSITIVE : ($code == 0) ? RSVP::NEGATIVE : null; @@ -189,7 +203,11 @@ class RSVP extends Managed_DataObject function getNotice() { - return Notice::staticGet('uri', $this->uri); + $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) @@ -207,11 +225,15 @@ class RSVP extends Managed_DataObject if ($rsvp->find()) { while ($rsvp->fetch()) { - $verb = $this->verbFor($rsvp->code); + $verb = self::verbFor($rsvp->result); $rsvps[$verb][] = clone($rsvp); } } return $rsvps; } + + function delete() + { + } } diff --git a/plugins/Event/cancelrsvp.php b/plugins/Event/cancelrsvp.php new file mode 100644 index 0000000000..21ed41a451 --- /dev/null +++ b/plugins/Event/cancelrsvp.php @@ -0,0 +1,202 @@ +. + * + * @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); + + $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)) { + $notice->delete(); + } else { + $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/newevent.php b/plugins/Event/newevent.php index 7c0fd0177f..0f5635487b 100644 --- a/plugins/Event/newevent.php +++ b/plugins/Event/newevent.php @@ -157,6 +157,10 @@ class NeweventAction extends Action $this->description, $this->url); + $event = Happening::fromNotice($saved); + + RSVP::saveNew($profile, $event, RSVP::POSITIVE); + } catch (ClientException $ce) { $this->error = $ce->getMessage(); $this->showPage(); diff --git a/plugins/Event/newrsvp.php b/plugins/Event/newrsvp.php index a793ac6de2..da613ec6c7 100644 --- a/plugins/Event/newrsvp.php +++ b/plugins/Event/newrsvp.php @@ -1,17 +1,11 @@ - * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3 - * @link http://status.net/ - * * StatusNet - the distributed open-source microblogging tool - * Copyright (C) 2009, StatusNet, Inc. + * Copyright (C) 2011, StatusNet, Inc. + * + * RSVP for an event + * + * PHP version 5 * * 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 @@ -25,140 +19,184 @@ * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . + * + * @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); } /** - * Give a warm greeting to our friendly user + * RSVP for an event * - * This sample action shows some basic ways of doing output in an action - * class. - * - * Action classes have several output methods that they override from - * the parent class. - * - * @category Sample - * @package StatusNet - * @author Evan Prodromou - * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3 - * @link http://status.net/ + * @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 HelloAction extends Action + +class NewrsvpAction extends Action { - var $user = null; - var $gc = null; + protected $user = null; + protected $event = null; + protected $type = null; /** - * Take arguments for running + * Returns the title of the action * - * This method is called first, and it lets the action class get - * all its arguments and validate them. It's also the time - * to fetch any relevant data from the database. - * - * Action classes should run parent::prepare($args) as the first - * line of this method to make sure the default argument-processing - * happens. - * - * @param array $args $_REQUEST args - * - * @return boolean success flag + * @return string Action title */ - function prepare($args) + + function title() { - parent::prepare($args); + return _('New RSVP'); + } + + /** + * For initializing members of the class. + * + * @param array $argarray misc. arguments + * + * @return boolean true + */ + + function prepare($argarray) + { + parent::prepare($argarray); + + $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)) { - $this->gc = User_greeting_count::inc($this->user->id); + 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; } /** - * Handle request + * Handler method * - * This is the main method for handling a request. Note that - * most preparation should be done in the prepare() method; - * by the time handle() is called the action should be - * more or less ready to go. - * - * @param array $args $_REQUEST args; handled in prepare() + * @param array $argarray is ignored since it's now passed in in prepare() * * @return void */ - function handle($args) - { - parent::handle($args); - $this->showPage(); + function handle($argarray=null) + { + parent::handle($argarray); + + if ($this->isPost()) { + $this->newRSVP(); + } else { + $this->showPage(); + } + + return; } /** - * Title of this page + * Add a new event * - * Override this method to show a custom title. - * - * @return string Title of the page + * @return void */ - function title() + + function newRSVP() { - if (empty($this->user)) { - return _m('Hello'); + 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 { - return sprintf(_m('Hello, %s!'), $this->user->nickname); + common_redirect($saved->bestUrl(), 303); } } /** - * Show content in the content area - * - * The default StatusNet page has a lot of decorations: menus, - * logos, tabs, all that jazz. This method is used to show - * content in the content area of the page; it's the main - * thing you want to overload. - * - * This method also demonstrates use of a plural localized string. + * Show the event form * * @return void */ + function showContent() { - if (empty($this->user)) { - $this->element('p', array('class' => 'greeting'), - _m('Hello, stranger!')); - } else { - $this->element('p', array('class' => 'greeting'), - sprintf(_m('Hello, %s'), $this->user->nickname)); - $this->element('p', array('class' => 'greeting_count'), - sprintf(_m('I have greeted you %d time.', - 'I have greeted you %d times.', - $this->gc->greeting_count), - $this->gc->greeting_count)); + if (!empty($this->error)) { + $this->element('p', 'error', $this->error); } + + $form = new RSVPForm($this->event, $this); + + $form->show(); + + return; } /** * Return true if read only. * - * Some actions only read from the database; others read and write. - * The simple database load-balancer built into StatusNet will - * direct read-only actions to database mirrors (if they are configured), - * and read-write actions to the master database. + * MAY override * - * This defaults to false to avoid data integrity issues, but you - * should make sure to overload it for performance gains. - * - * @param array $args other arguments, if RO/RW status depends on them. + * @param array $args other arguments * * @return boolean is read only action? */ + function isReadOnly($args) { - return false; + 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 index f8b032c111..7fb702f9db 100644 --- a/plugins/Event/showevent.php +++ b/plugins/Event/showevent.php @@ -64,13 +64,13 @@ class ShoweventAction extends ShownoticeAction $this->id = $this->trimmed('id'); - $this->event = Event::staticGet('id', $this->id); + $this->event = Happening::staticGet('id', $this->id); if (empty($this->event)) { throw new ClientException(_('No such event.'), 404); } - $this->notice = $event->getNotice(); + $this->notice = $this->event->getNotice(); if (empty($this->notice)) { // Did we used to have it, and it got deleted? From 1f86f45bb9e349f23a000319b2cac19759c0b218 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Wed, 9 Mar 2011 10:38:44 -0800 Subject: [PATCH 19/29] Move object menu into aside --- lib/action.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/action.php b/lib/action.php index 28b0fdbacf..92be43ba68 100644 --- a/lib/action.php +++ b/lib/action.php @@ -669,10 +669,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)); @@ -828,6 +824,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)); From 7f42e48631049ae4c29166684f2c1aa0ad5e2f04 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Wed, 9 Mar 2011 10:53:45 -0800 Subject: [PATCH 20/29] section class on object nav --- lib/action.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/action.php b/lib/action.php index 92be43ba68..ca380cc02c 100644 --- a/lib/action.php +++ b/lib/action.php @@ -712,7 +712,8 @@ class Action extends HTMLOutputter // lawsuit { // 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->elementStart('div', array('id' => 'site_nav_object', + 'class' => 'section')); $this->showObjectNav(); $this->elementEnd('div'); } From 0a19949f6db090fa529eb8d7a42fa313ebf87561 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Wed, 9 Mar 2011 10:53:12 -0800 Subject: [PATCH 21/29] Allow theme.ini to specify external CSS URLs, such as Google Font API loaders. Example theme.ini: external="http://fonts.googleapis.com/css?family=Lato:100,100italic,300,300italic,400,400italic,700,700italic,900,900italic" include=rebase Notes: * URLs must be quoted in the .ini file or the .ini file parser explodes! * To do multiples, list as external[] instead of external. * If there's an included base theme, any externals it lists will be included first. * All externals are loaded before any local styles. --- lib/action.php | 9 ++++++++- lib/theme.php | 52 +++++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 57 insertions(+), 4 deletions(-) diff --git a/lib/action.php b/lib/action.php index ca380cc02c..ea3b50a229 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); 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 * From 1f9a9c69bc3f5d7b33f4f73da3252ec07b5f3a87 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Wed, 9 Mar 2011 12:07:26 -0800 Subject: [PATCH 22/29] Only show the site_nav_object block if Action::showObjectNav has been overridden to do something. (Copied similar check from showPageNoticeBlock, handy!) --- lib/action.php | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/lib/action.php b/lib/action.php index ea3b50a229..2b237118af 100644 --- a/lib/action.php +++ b/lib/action.php @@ -713,16 +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', - 'class' => 'section')); - $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'); + } } /** From 381ffc3fe8c4048761128103d4dfcb153d75464e Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Wed, 9 Mar 2011 12:38:06 -0800 Subject: [PATCH 23/29] Stub 'home' subsection at top of settings & admin nav panels, so folks always know how to get home. (If we drop the section titles, these'll look a little cleaner since it'll only show 'Home' once :D) --- lib/adminpanelnav.php | 19 +++++++++++++++++++ lib/settingsnav.php | 20 ++++++++++++++++++++ 2 files changed, 39 insertions(+) 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/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'); } } From 040baf291d031b20723ec5455e120b3e40c98a49 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Wed, 9 Mar 2011 13:05:12 -0800 Subject: [PATCH 24/29] Fix for input tab selection --- js/util.js | 7 +++++++ lib/action.php | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/js/util.js b/js/util.js index 6bba34fae6..6bd5d668a9 100644 --- a/js/util.js +++ b/js/util.js @@ -1280,6 +1280,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'); diff --git a/lib/action.php b/lib/action.php index 2b237118af..0ba4b8b8ff 100644 --- a/lib/action.php +++ b/lib/action.php @@ -602,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); From e9cab215fe5789ac26a1feab3d4f67f2f97ae4b9 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Wed, 9 Mar 2011 13:31:22 -0800 Subject: [PATCH 25/29] Event: fix for display problem -- need to leave this div open for now --- plugins/Event/EventPlugin.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/Event/EventPlugin.php b/plugins/Event/EventPlugin.php index 7ca2fa9c0e..bd9c10b3e4 100644 --- a/plugins/Event/EventPlugin.php +++ b/plugins/Event/EventPlugin.php @@ -391,7 +391,8 @@ class EventPlugin extends MicroappPlugin $profile->nickname); $out->elementEnd('span'); - $out->elementEnd('div'); + // @fixme right now we have to leave this div open + //$out->elementEnd('div'); } /** From 4ea35f339d9f926c1c1f50c4b11a9149e8e524f4 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Wed, 9 Mar 2011 14:10:26 -0800 Subject: [PATCH 26/29] Fixes for Event plugin: * RSVP cancel/delete now works * caching fix for RSVP insert and delete (compound unique keys aren't properly handled for pkeyGet's caching right now; hacked it for this class for the moment) * div nesting fix * missing name/avatar on RSVP responses --- plugins/Event/EventPlugin.php | 67 +++++++++++++++++++---------------- plugins/Event/Happening.php | 1 + plugins/Event/RSVP.php | 18 +++++++--- plugins/Event/cancelrsvp.php | 5 +++ plugins/Event/newrsvp.php | 3 ++ 5 files changed, 60 insertions(+), 34 deletions(-) diff --git a/plugins/Event/EventPlugin.php b/plugins/Event/EventPlugin.php index bd9c10b3e4..5c2fd35d74 100644 --- a/plugins/Event/EventPlugin.php +++ b/plugins/Event/EventPlugin.php @@ -295,6 +295,31 @@ class EventPlugin extends MicroappPlugin $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) @@ -311,9 +336,9 @@ class EventPlugin extends MicroappPlugin assert(!empty($event)); assert(!empty($profile)); - $out->elementStart('div', 'vevent'); + $out->elementStart('div', 'vevent'); // VEVENT IN - $out->elementStart('h3'); + $out->elementStart('h3'); // VEVENT/H3 IN if (!empty($event->url)) { $out->element('a', @@ -324,11 +349,11 @@ class EventPlugin extends MicroappPlugin $out->text($event->title); } - $out->elementEnd('h3'); + $out->elementEnd('h3'); // VEVENT/H3 OUT // FIXME: better dates - $out->elementStart('div', 'event-times'); + $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)); @@ -336,7 +361,7 @@ class EventPlugin extends MicroappPlugin $out->element('span', array('class' => 'dtend', 'title' => common_date_iso8601($event->end_time)), common_exact_date($event->end_time)); - $out->elementEnd('div'); + $out->elementEnd('div'); // VEVENT/EVENT-TIMES OUT if (!empty($event->description)) { $out->element('div', 'description', $event->description); @@ -358,6 +383,7 @@ class EventPlugin extends MicroappPlugin 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); @@ -368,31 +394,7 @@ class EventPlugin extends MicroappPlugin $form->show(); } - $out->elementStart('div', array('class' => 'event-info entry-content')); - - $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'); - - // @fixme right now we have to leave this div open - //$out->elementEnd('div'); + $out->elementEnd('div'); // vevent out } /** @@ -417,15 +419,20 @@ class EventPlugin extends MicroappPlugin { 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 index 1a6a028dca..376f27c698 100644 --- a/plugins/Event/Happening.php +++ b/plugins/Event/Happening.php @@ -213,6 +213,7 @@ class Happening extends Managed_DataObject 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 index 22bd239a68..c61ff3dbf0 100644 --- a/plugins/Event/RSVP.php +++ b/plugins/Event/RSVP.php @@ -85,6 +85,20 @@ class RSVP extends Managed_DataObject 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. */ @@ -232,8 +246,4 @@ class RSVP extends Managed_DataObject return $rsvps; } - - function delete() - { - } } diff --git a/plugins/Event/cancelrsvp.php b/plugins/Event/cancelrsvp.php index 21ed41a451..83dabe2de5 100644 --- a/plugins/Event/cancelrsvp.php +++ b/plugins/Event/cancelrsvp.php @@ -72,6 +72,9 @@ class CancelrsvpAction extends Action function prepare($argarray) { parent::prepare($argarray); + if ($this->boolean('ajax')) { + StatusNet::setApi(true); // short error results! + } $rsvpId = $this->trimmed('rsvp'); @@ -133,8 +136,10 @@ class CancelrsvpAction extends Action $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) { diff --git a/plugins/Event/newrsvp.php b/plugins/Event/newrsvp.php index da613ec6c7..4bacd129f4 100644 --- a/plugins/Event/newrsvp.php +++ b/plugins/Event/newrsvp.php @@ -72,6 +72,9 @@ class NewrsvpAction extends Action function prepare($argarray) { parent::prepare($argarray); + if ($this->boolean('ajax')) { + StatusNet::setApi(true); // short error results! + } $eventId = $this->trimmed('event'); From e74590d41465b691cc49f4ae50fc9d9013293a91 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Wed, 9 Mar 2011 15:46:24 -0800 Subject: [PATCH 27/29] Ticket #3076: fix regression in password recovery when email address given that doesn't match Was triggering errors due to use of common_canonical_nickname() on arbitrary input without checking for exceptions about invalid nicknames (which didn't exist long ago in the before time) --- actions/recoverpassword.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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 From cecc2576a57f75421df2cdc2416d5f3940967f64 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Wed, 9 Mar 2011 16:55:52 -0800 Subject: [PATCH 28/29] Fixes for direct message form for JS changes to the main form --- js/util.js | 6 +++--- js/util.min.js | 2 +- lib/messageform.php | 4 ++-- lib/noticeform.php | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/js/util.js b/js/util.js index 6bd5d668a9..37a7585d23 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; } diff --git a/js/util.min.js b/js/util.min.js index 88db1a1ac6..07994af6d1 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(".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.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.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(".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.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/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 e889036f55..9d931b92ed 100644 --- a/lib/noticeform.php +++ b/lib/noticeform.php @@ -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'), From ec828a094cf187a6650fa5325d1591351f031323 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Wed, 9 Mar 2011 17:43:31 -0800 Subject: [PATCH 29/29] Update LinkPreview plugin for multiple notice forms. * main notice form setup now encapsulated into SN.Init.NoticeForm(form) -- this can be monkeypatched by plugins to append their own setup code, as LinkPreview does * LinkPreview now supports debugging with non-minified JS source when $config['site']['minify'] is false * tweaked core & neo styles so 'notice-status' class gets same styles as attach-status, so we can more easily add mroe statusy things. (needs more consolidation with geo-status, etc) * tweaked LinkPreview's preview area to use that style --- js/util.js | 24 +- js/util.min.js | 2 +- plugins/LinkPreview/LinkPreviewPlugin.php | 7 +- plugins/LinkPreview/linkpreview.js | 343 ++++++++++++---------- plugins/LinkPreview/linkpreview.min.js | 2 +- theme/base/css/display.css | 6 +- theme/neo/css/display.css | 7 +- theme/rebase/css/display.css | 6 +- 8 files changed, 219 insertions(+), 178 deletions(-) diff --git a/js/util.js b/js/util.js index 37a7585d23..cc95a08bf5 100644 --- a/js/util.js +++ b/js/util.js @@ -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(); }; @@ -1310,14 +1307,25 @@ var SN = { // StatusNet if ($('body.user_in').length > 0) { $('.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 07994af6d1..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(".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.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(".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.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/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/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; }