diff --git a/actions/login.php b/actions/login.php index 768bc04cef..547374a12e 100644 --- a/actions/login.php +++ b/actions/login.php @@ -297,4 +297,8 @@ class LoginAction extends Action $nav = new LoginGroupNav($this); $nav->show(); } + + function showNoticeForm() + { + } } diff --git a/actions/recoverpassword.php b/actions/recoverpassword.php index 9019d6fb22..a73872bfdb 100644 --- a/actions/recoverpassword.php +++ b/actions/recoverpassword.php @@ -282,7 +282,11 @@ class RecoverpasswordAction extends Action $user = User::staticGet('email', common_canonical_email($nore)); if (!$user) { - $user = User::staticGet('nickname', common_canonical_nickname($nore)); + try { + $user = User::staticGet('nickname', common_canonical_nickname($nore)); + } catch (NicknameException $e) { + // invalid + } } # See if it's an unconfirmed email address diff --git a/actions/register.php b/actions/register.php index 6b039c93f6..d0dbceeb81 100644 --- a/actions/register.php +++ b/actions/register.php @@ -606,4 +606,8 @@ class RegisterAction extends Action $nav = new LoginGroupNav($this); $nav->show(); } + + function showNoticeForm() + { + } } diff --git a/js/util.js b/js/util.js index dcb8da4600..cc95a08bf5 100644 --- a/js/util.js +++ b/js/util.js @@ -104,7 +104,7 @@ var SN = { // StatusNet SN.U.Counter(form); - NDT = form.find('[name=status_textarea]'); + NDT = form.find('.notice_data-text:first'); NDT.bind('keyup', function(e) { SN.U.Counter(form); @@ -183,7 +183,7 @@ var SN = { // StatusNet * @return number of chars */ CharacterCount: function(form) { - return form.find('[name=status_textarea]').val().length; + return form.find('.notice_data-text:first').val().length; }, /** @@ -327,7 +327,7 @@ var SN = { // StatusNet dataType: 'xml', timeout: '60000', beforeSend: function(formData) { - if (form.find('[name=status_textarea]').val() == '') { + if (form.find('.notice_data-text:first').val() == '') { form.addClass(SN.C.S.Warning); return false; } @@ -612,10 +612,7 @@ var SN = { // StatusNet list.append(replyItem); var form = replyForm = $(formEl); - SN.U.NoticeLocationAttach(form); - SN.U.FormNoticeXHR(form); - SN.U.FormNoticeEnhancements(form); - SN.U.NoticeDataAttach(form); + SN.Init.NoticeFormSetup(form); nextStep(); }; @@ -1263,7 +1260,7 @@ var SN = { // StatusNet var profileLink = $('#nav_profile a').attr('href'); if (profileLink) { - var authorUrl = $(notice).find('.entry-title .author a.url').attr('href'); + var authorUrl = $(notice).find('.vcard.author a.url').attr('href'); if (authorUrl == profileLink) { if (action == 'all' || action == 'showstream') { // Posts always show on your own friends and profile streams. @@ -1280,6 +1277,13 @@ var SN = { // StatusNet return false; }, + /** + * Switch to another active input sub-form. + * This will hide the current form (if any), show the new one, and + * update the input type tab selection state. + * + * @param {String} tag + */ switchInputFormTab: function(tag) { // The one that's current isn't current anymore $('.input_form_nav_tab.current').removeClass('current'); @@ -1301,16 +1305,27 @@ var SN = { // StatusNet */ NoticeForm: function() { if ($('body.user_in').length > 0) { - $('.'+SN.C.S.FormNotice).each(function() { + $('.ajax-notice').each(function() { var form = $(this); - SN.U.NoticeLocationAttach(form); - SN.U.FormNoticeXHR(form); - SN.U.FormNoticeEnhancements(form); - SN.U.NoticeDataAttach(form); + SN.Init.NoticeFormSetup(form); }); } }, + /** + * Encapsulate notice form setup for a single form. + * Plugins can add extra setup by monkeypatching this + * function. + * + * @param {jQuery} form + */ + NoticeFormSetup: function(form) { + SN.U.NoticeLocationAttach(form); + SN.U.FormNoticeXHR(form); + SN.U.FormNoticeEnhancements(form); + SN.U.NoticeDataAttach(form); + }, + /** * Run setup code for notice timeline views items: * diff --git a/js/util.min.js b/js/util.min.js index 5e08cc78b5..32a893355f 100644 --- a/js/util.min.js +++ b/js/util.min.js @@ -1 +1 @@ -var SN={C:{I:{CounterBlackout:false,MaxLength:140,PatternUsername:/^[0-9a-zA-Z\-_.]*$/,HTTP20x30x:[200,201,202,203,204,205,206,300,301,302,303,304,305,306,307],NoticeFormMaster:null},S:{Disabled:"disabled",Warning:"warning",Error:"error",Success:"success",Processing:"processing",CommandResult:"command_result",FormNotice:"form_notice",NoticeDataGeo:"notice_data-geo",NoticeDataGeoCookie:"NoticeDataGeo",NoticeDataGeoSelected:"notice_data-geo_selected",StatusNetInstance:"StatusNetInstance"}},messages:{},msg:function(a){if(typeof SN.messages[a]=="undefined"){return"["+a+"]"}else{return SN.messages[a]}},U:{FormNoticeEnhancements:function(b){if(jQuery.data(b[0],"ElementData")===undefined){MaxLength=b.find(".count").text();if(typeof(MaxLength)=="undefined"){MaxLength=SN.C.I.MaxLength}jQuery.data(b[0],"ElementData",{MaxLength:MaxLength});SN.U.Counter(b);NDT=b.find("[name=status_textarea]");NDT.bind("keyup",function(c){SN.U.Counter(b)});var a=function(c){window.setTimeout(function(){SN.U.Counter(b)},50)};NDT.bind("cut",a).bind("paste",a)}else{b.find(".count").text(jQuery.data(b[0],"ElementData").MaxLength)}},Counter:function(d){SN.C.I.FormNoticeCurrent=d;var b=jQuery.data(d[0],"ElementData").MaxLength;if(b<=0){return}var c=b-SN.U.CharacterCount(d);var a=d.find(".count");if(c.toString()!=a.text()){if(!SN.C.I.CounterBlackout||c===0){if(a.text()!=String(c)){a.text(c)}if(c<0){d.addClass(SN.C.S.Warning)}else{d.removeClass(SN.C.S.Warning)}if(!SN.C.I.CounterBlackout){SN.C.I.CounterBlackout=true;SN.C.I.FormNoticeCurrent=d;window.setTimeout("SN.U.ClearCounterBlackout(SN.C.I.FormNoticeCurrent);",500)}}}},CharacterCount:function(a){return a.find("[name=status_textarea]").val().length},ClearCounterBlackout:function(a){SN.C.I.CounterBlackout=false;SN.U.Counter(a)},RewriteAjaxAction:function(a){if(document.location.protocol=="https:"&&a.substr(0,5)=="http:"){return a.replace(/^http:\/\/[^:\/]+/,"https://"+document.location.host)}else{return a}},FormXHR:function(a){$.ajax({type:"POST",dataType:"xml",url:SN.U.RewriteAjaxAction(a.attr("action")),data:a.serialize()+"&ajax=1",beforeSend:function(b){a.addClass(SN.C.S.Processing).find(".submit").addClass(SN.C.S.Disabled).attr(SN.C.S.Disabled,SN.C.S.Disabled)},error:function(c,d,b){alert(b||d)},success:function(b,c){if(typeof($("form",b)[0])!="undefined"){form_new=document._importNode($("form",b)[0],true);a.replaceWith(form_new)}else{a.replaceWith(document._importNode($("p",b)[0],true))}}})},FormNoticeXHR:function(b){SN.C.I.NoticeDataGeo={};b.append('');b.attr("action",SN.U.RewriteAjaxAction(b.attr("action")));var c=function(d,e){b.append($('

').addClass(d).text(e))};var a=function(){b.find(".form_response").remove()};b.ajaxForm({dataType:"xml",timeout:"60000",beforeSend:function(d){if(b.find("[name=status_textarea]").val()==""){b.addClass(SN.C.S.Warning);return false}b.addClass(SN.C.S.Processing).find(".submit").addClass(SN.C.S.Disabled).attr(SN.C.S.Disabled,SN.C.S.Disabled);SN.U.normalizeGeoData(b);return true},error:function(f,g,e){b.removeClass(SN.C.S.Processing).find(".submit").removeClass(SN.C.S.Disabled).removeAttr(SN.C.S.Disabled,SN.C.S.Disabled);a();if(g=="timeout"){c("error","Sorry! We had trouble sending your notice. The servers are overloaded. Please try again, and contact the site administrator if this problem persists.")}else{var d=SN.U.GetResponseXML(f);if($("."+SN.C.S.Error,d).length>0){b.append(document._importNode($("."+SN.C.S.Error,d)[0],true))}else{if(parseInt(f.status)===0||jQuery.inArray(parseInt(f.status),SN.C.I.HTTP20x30x)>=0){b.resetForm().find(".attach-status").remove();SN.U.FormNoticeEnhancements(b)}else{c("error","(Sorry! We had trouble sending your notice ("+f.status+" "+f.statusText+"). Please report the problem to the site administrator if this happens again.")}}}},success:function(i,f){a();var n=$("#"+SN.C.S.Error,i);if(n.length>0){c("error",n.text())}else{if($("body")[0].id=="bookmarklet"){self.close()}var d=$("#"+SN.C.S.CommandResult,i);if(d.length>0){c("success",d.text())}else{var m=document._importNode($("li",i)[0],true);var k=$("#notices_primary .notices:first");var l=b.closest("li.notice-reply");if(l.length>0){var e=$(m).attr("id");if($("#"+e).length==0){var j=l.closest("li.notice");l.replaceWith(m);SN.U.NoticeInlineReplyPlaceholder(j)}else{l.remove()}}else{if(k.length>0&&SN.U.belongsOnTimeline(m)){if($("#"+m.id).length===0){var h=b.find("[name=inreplyto]").val();var g="#notices_primary #notice-"+h;if($("body")[0].id=="conversation"){if(h.length>0&&$(g+" .notices").length<1){$(g).append('')}$($(g+" .notices")[0]).append(m)}else{k.prepend(m)}$("#"+m.id).css({display:"none"}).fadeIn(2500);SN.U.NoticeWithAttachment($("#"+m.id));SN.U.NoticeReplyTo($("#"+m.id))}}else{c("success",$("title",i).text())}}}b.resetForm();b.find("[name=inreplyto]").val("");b.find(".attach-status").remove();SN.U.FormNoticeEnhancements(b)}},complete:function(d,e){b.removeClass(SN.C.S.Processing).find(".submit").removeAttr(SN.C.S.Disabled).removeClass(SN.C.S.Disabled);b.find("[name=lat]").val(SN.C.I.NoticeDataGeo.NLat);b.find("[name=lon]").val(SN.C.I.NoticeDataGeo.NLon);b.find("[name=location_ns]").val(SN.C.I.NoticeDataGeo.NLNS);b.find("[name=location_id]").val(SN.C.I.NoticeDataGeo.NLID);b.find("[name=notice_data-geo]").attr("checked",SN.C.I.NoticeDataGeo.NDG)}})},normalizeGeoData:function(a){SN.C.I.NoticeDataGeo.NLat=a.find("[name=lat]").val();SN.C.I.NoticeDataGeo.NLon=a.find("[name=lon]").val();SN.C.I.NoticeDataGeo.NLNS=a.find("[name=location_ns]").val();SN.C.I.NoticeDataGeo.NLID=a.find("[name=location_id]").val();SN.C.I.NoticeDataGeo.NDG=a.find("[name=notice_data-geo]").attr("checked");var b=$.cookie(SN.C.S.NoticeDataGeoCookie);if(b!==null&&b!="disabled"){b=JSON.parse(b);SN.C.I.NoticeDataGeo.NLat=a.find("[name=lat]").val(b.NLat).val();SN.C.I.NoticeDataGeo.NLon=a.find("[name=lon]").val(b.NLon).val();if(b.NLNS){SN.C.I.NoticeDataGeo.NLNS=a.find("[name=location_ns]").val(b.NLNS).val();SN.C.I.NoticeDataGeo.NLID=a.find("[name=location_id]").val(b.NLID).val()}else{a.find("[name=location_ns]").val("");a.find("[name=location_id]").val("")}}if(b=="disabled"){SN.C.I.NoticeDataGeo.NDG=a.find("[name=notice_data-geo]").attr("checked",false).attr("checked")}else{SN.C.I.NoticeDataGeo.NDG=a.find("[name=notice_data-geo]").attr("checked",true).attr("checked")}},GetResponseXML:function(b){try{return b.responseXML}catch(a){return(new DOMParser()).parseFromString(b.responseText,"text/xml")}},NoticeReply:function(){if($("#content .notice_reply").length>0){$("#content .notice").each(function(){SN.U.NoticeReplyTo($(this))})}},NoticeReplyTo:function(a){a.find(".notice_reply").live("click",function(c){c.preventDefault();var b=($(".author .nickname",a).length>0)?$($(".author .nickname",a)[0]):$(".author .nickname.uid");SN.U.NoticeInlineReplyTrigger(a,"@"+b.text());return false})},NoticeInlineReplyTrigger:function(h,i){var b=$($(".notice_id",h)[0]).text();var e=h;var f=h.closest(".notices");if(f.hasClass("threaded-replies")){e=f.closest(".notice")}else{f=$("ul.threaded-replies",h);if(f.length==0){f=$('');h.append(f)}}var j=$(".notice-reply-form",f);var d=function(){j.find("input[name=inreplyto]").val(b);var m=j.find("textarea");if(m.length==0){throw"No textarea"}var l="";if(i){l=i+" "}m.val(l+m.val().replace(RegExp(l,"i"),""));m.data("initialText",$.trim(i+""));m.focus();if(m[0].setSelectionRange){var k=m.val().length;m[0].setSelectionRange(k,k)}};if(j.length>0){d()}else{$("li.notice-reply-placeholder").remove();var g=$("li.notice-reply",f);if(g.length==0){g=$('
  • ');var c=function(k){var l=document._importNode(k,true);g.append(l);f.append(g);var m=j=$(l);SN.U.NoticeLocationAttach(m);SN.U.FormNoticeXHR(m);SN.U.FormNoticeEnhancements(m);SN.U.NoticeDataAttach(m);d()};if(SN.C.I.NoticeFormMaster){c(SN.C.I.NoticeFormMaster)}else{var a=$("#form_notice").attr("action");$.get(a,{ajax:1},function(k,m,l){c($("form",k)[0])})}}}},NoticeInlineReplyPlaceholder:function(b){var a=b.find("ul.threaded-replies");var c=$('
  • ');c.click(function(){SN.U.NoticeInlineReplyTrigger(b)});c.find("input").val(SN.msg("reply_placeholder"));a.append(c)},NoticeInlineReplySetup:function(){$(".threaded-replies").each(function(){var b=$(this);var a=b.closest(".notice");SN.U.NoticeInlineReplyPlaceholder(a)})},NoticeRepeat:function(){$(".form_repeat").live("click",function(a){a.preventDefault();SN.U.NoticeRepeatConfirmation($(this));return false})},NoticeRepeatConfirmation:function(a){var c=a.find(".submit");var b=c.clone();b.addClass("submit_dialogbox").removeClass("submit");a.append(b);b.bind("click",function(){SN.U.FormXHR(a);return false});c.hide();a.addClass("dialogbox").append('').closest(".notice-options").addClass("opaque");a.find("button.close").click(function(){$(this).remove();a.removeClass("dialogbox").closest(".notice-options").removeClass("opaque");a.find(".submit_dialogbox").remove();a.find(".submit").show();return false})},NoticeAttachments:function(){$(".notice a.attachment").each(function(){SN.U.NoticeWithAttachment($(this).closest(".notice"))})},NoticeWithAttachment:function(b){if(b.find(".attachment").length===0){return}var a=b.find(".attachment.more");if(a.length>0){$(a[0]).click(function(){var c=$(this);c.addClass(SN.C.S.Processing);$.get(c.attr("href")+"/ajax",null,function(d){c.parent(".entry-content").html($(d).find("#attachment_view .entry-content").html())});return false}).attr("title",SN.msg("showmore_tooltip"))}},NoticeDataAttach:function(b){var a=b.find("input[type=file]");a.change(function(f){b.find(".attach-status").remove();var d=$(this).val();if(!d){return false}var c=$('
    ');c.find("code").text(d);c.find("button").click(function(){c.remove();a.val("");return false});b.append(c);if(typeof this.files=="object"){for(var e=0;eg){f=false}if(f){h(c,function(j){var i=$("").attr("title",e).attr("alt",e).attr("src",j).attr("style","height: 120px");d.find(".attach-status").append(i)})}else{var b=$("
    ").text(e);d.find(".attach-status").append(b)}},NoticeLocationAttach:function(a){var e=a.find("[name=lat]");var k=a.find("[name=lon]");var g=a.find("[name=location_ns]").val();var l=a.find("[name=location_id]").val();var b="";var d=a.find("[name=notice_data-geo]");var c=a.find("[name=notice_data-geo]");var j=a.find("label.notice_data-geo");function f(n){j.attr("title",jQuery.trim(j.text())).removeClass("checked");a.find("[name=lat]").val("");a.find("[name=lon]").val("");a.find("[name=location_ns]").val("");a.find("[name=location_id]").val("");a.find("[name=notice_data-geo]").attr("checked",false);$.cookie(SN.C.S.NoticeDataGeoCookie,"disabled",{path:"/"});if(n){a.find(".geo_status_wrapper").removeClass("success").addClass("error");a.find(".geo_status_wrapper .geo_status").text(n)}else{a.find(".geo_status_wrapper").remove()}}function m(n,o){SN.U.NoticeGeoStatus(a,"Looking up place name...");$.getJSON(n,o,function(p){var q,r;if(typeof(p.location_ns)!="undefined"){a.find("[name=location_ns]").val(p.location_ns);q=p.location_ns}if(typeof(p.location_id)!="undefined"){a.find("[name=location_id]").val(p.location_id);r=p.location_id}if(typeof(p.name)=="undefined"){NLN_text=o.lat+";"+o.lon}else{NLN_text=p.name}SN.U.NoticeGeoStatus(a,NLN_text,o.lat,o.lon,p.url);j.attr("title",NoticeDataGeo_text.ShareDisable+" ("+NLN_text+")");a.find("[name=lat]").val(o.lat);a.find("[name=lon]").val(o.lon);a.find("[name=location_ns]").val(q);a.find("[name=location_id]").val(r);a.find("[name=notice_data-geo]").attr("checked",true);var s={NLat:o.lat,NLon:o.lon,NLNS:q,NLID:r,NLN:NLN_text,NLNU:p.url,NDG:true};$.cookie(SN.C.S.NoticeDataGeoCookie,JSON.stringify(s),{path:"/"})})}if(c.length>0){if($.cookie(SN.C.S.NoticeDataGeoCookie)=="disabled"){c.attr("checked",false)}else{c.attr("checked",true)}var h=a.find(".notice_data-geo_wrap");var i=h.attr("title");h.removeAttr("title");j.attr("title",j.text());c.change(function(){if(c.attr("checked")===true||$.cookie(SN.C.S.NoticeDataGeoCookie)===null){j.attr("title",NoticeDataGeo_text.ShareDisable).addClass("checked");if($.cookie(SN.C.S.NoticeDataGeoCookie)===null||$.cookie(SN.C.S.NoticeDataGeoCookie)=="disabled"){if(navigator.geolocation){SN.U.NoticeGeoStatus(a,"Requesting location from browser...");navigator.geolocation.getCurrentPosition(function(p){a.find("[name=lat]").val(p.coords.latitude);a.find("[name=lon]").val(p.coords.longitude);var q={lat:p.coords.latitude,lon:p.coords.longitude,token:$("#token").val()};m(i,q)},function(p){switch(p.code){case p.PERMISSION_DENIED:f("Location permission denied.");break;case p.TIMEOUT:f("Location lookup timeout.");break}},{timeout:10000})}else{if(e.length>0&&k.length>0){var n={lat:e,lon:k,token:$("#token").val()};m(i,n)}else{f();c.remove();j.remove()}}}else{var o=JSON.parse($.cookie(SN.C.S.NoticeDataGeoCookie));a.find("[name=lat]").val(o.NLat);a.find("[name=lon]").val(o.NLon);a.find("[name=location_ns]").val(o.NLNS);a.find("[name=location_id]").val(o.NLID);a.find("[name=notice_data-geo]").attr("checked",o.NDG);SN.U.NoticeGeoStatus(a,o.NLN,o.NLat,o.NLon,o.NLNU);j.attr("title",NoticeDataGeo_text.ShareDisable+" ("+o.NLN+")").addClass("checked")}}else{f()}}).change()}},NoticeGeoStatus:function(e,a,f,g,c){var h=e.find(".geo_status_wrapper");if(h.length==0){h=$('
    ');h.find("button.close").click(function(){e.find("[name=notice_data-geo]").removeAttr("checked").change()});e.append(h)}var b;if(c){b=$("").attr("href",c)}else{b=$("")}b.text(a);if(f||g){var d=f+";"+g;b.attr("title",d);if(!a){b.text(d)}}h.find(".geo_status").empty().append(b)},NewDirectMessage:function(){NDM=$(".entity_send-a-message a");NDM.attr({href:NDM.attr("href")+"&ajax=1"});NDM.bind("click",function(){var a=$(".entity_send-a-message form");if(a.length===0){$(this).addClass(SN.C.S.Processing);$.get(NDM.attr("href"),null,function(b){$(".entity_send-a-message").append(document._importNode($("form",b)[0],true));a=$(".entity_send-a-message .form_notice");SN.U.FormNoticeXHR(a);SN.U.FormNoticeEnhancements(a);a.append('');$(".entity_send-a-message button").click(function(){a.hide();return false});NDM.removeClass(SN.C.S.Processing)})}else{a.show();$(".entity_send-a-message textarea").focus()}return false})},GetFullYear:function(c,d,a){var b=new Date();b.setFullYear(c,d,a);return b},StatusNetInstance:{Set:function(b){var a=SN.U.StatusNetInstance.Get();if(a!==null){b=$.extend(a,b)}$.cookie(SN.C.S.StatusNetInstance,JSON.stringify(b),{path:"/",expires:SN.U.GetFullYear(2029,0,1)})},Get:function(){var a=$.cookie(SN.C.S.StatusNetInstance);if(a!==null){return JSON.parse(a)}return null},Delete:function(){$.cookie(SN.C.S.StatusNetInstance,null)}},belongsOnTimeline:function(b){var a=$("body").attr("id");if(a=="public"){return true}var c=$("#nav_profile a").attr("href");if(c){var d=$(b).find(".entry-title .author a.url").attr("href");if(d==c){if(a=="all"||a=="showstream"){return true}}}return false},switchInputFormTab:function(a){$(".input_form_nav_tab.current").removeClass("current");$("#input_form_nav_"+a).addClass("current");$(".input_form.current").removeClass("current");$("#input_form_"+a).addClass("current")}},Init:{NoticeForm:function(){if($("body.user_in").length>0){$("."+SN.C.S.FormNotice).each(function(){var a=$(this);SN.U.NoticeLocationAttach(a);SN.U.FormNoticeXHR(a);SN.U.FormNoticeEnhancements(a);SN.U.NoticeDataAttach(a)})}},Notices:function(){if($("body.user_in").length>0){var a=$(".form_notice:first");if(a.length>0){SN.C.I.NoticeFormMaster=document._importNode(a[0],true)}SN.U.NoticeRepeat();SN.U.NoticeReply();SN.U.NoticeInlineReplySetup()}SN.U.NoticeAttachments()},EntityActions:function(){if($("body.user_in").length>0){SN.U.NewDirectMessage()}},Login:function(){if(SN.U.StatusNetInstance.Get()!==null){var a=SN.U.StatusNetInstance.Get().Nickname;if(a!==null){$("#form_login #nickname").val(a)}}$("#form_login").bind("submit",function(){SN.U.StatusNetInstance.Set({Nickname:$("#form_login #nickname").val()});return true})},AjaxForms:function(){$("form.ajax").live("submit",function(){SN.U.FormXHR($(this));return false})},UploadForms:function(){$("input[type=file]").change(function(d){if(typeof this.files=="object"&&this.files.length>0){var c=0;for(var b=0;b0&&c>a){var e="File too large: maximum upload size is %d bytes.";alert(e.replace("%d",a));$(this).val("");d.preventDefault();return false}}})}}};$(document).ready(function(){SN.Init.AjaxForms();SN.Init.UploadForms();if($("."+SN.C.S.FormNotice).length>0){SN.Init.NoticeForm()}if($("#content .notices").length>0){SN.Init.Notices()}if($("#content .entity_actions").length>0){SN.Init.EntityActions()}if($("#form_login").length>0){SN.Init.Login()}});if(!document.ELEMENT_NODE){document.ELEMENT_NODE=1;document.ATTRIBUTE_NODE=2;document.TEXT_NODE=3;document.CDATA_SECTION_NODE=4;document.ENTITY_REFERENCE_NODE=5;document.ENTITY_NODE=6;document.PROCESSING_INSTRUCTION_NODE=7;document.COMMENT_NODE=8;document.DOCUMENT_NODE=9;document.DOCUMENT_TYPE_NODE=10;document.DOCUMENT_FRAGMENT_NODE=11;document.NOTATION_NODE=12}document._importNode=function(e,a){switch(e.nodeType){case document.ELEMENT_NODE:var d=document.createElement(e.nodeName);if(e.attributes&&e.attributes.length>0){for(var c=0,b=e.attributes.length;c0){for(var c=0,b=e.childNodes.length;c0){var j=c.pop();j()}}};window._google_loader_apiLoaded=function(){f()};var d=function(){return(window.google&&google.loader)};var g=function(j){if(d()){return true}h(j);e();return false};e();return{shim:true,type:"ClientLocation",lastPosition:null,getCurrentPosition:function(k,n,o){var m=this;if(!g(function(){m.getCurrentPosition(k,n,o)})){return}if(google.loader.ClientLocation){var l=google.loader.ClientLocation;var j={coords:{latitude:l.latitude,longitude:l.longitude,altitude:null,accuracy:43000,altitudeAccuracy:null,heading:null,speed:null},address:{city:l.address.city,country:l.address.country,country_code:l.address.country_code,region:l.address.region},timestamp:new Date()};k(j);this.lastPosition=j}else{if(n==="function"){n({code:3,message:"Using the Google ClientLocation API and it is not able to calculate a location."})}}},watchPosition:function(j,l,m){this.getCurrentPosition(j,l,m);var k=this;var n=setInterval(function(){k.getCurrentPosition(j,l,m)},10000);return n},clearWatch:function(j){clearInterval(j)},getPermission:function(l,j,k){return true}}});navigator.geolocation=(window.google&&google.gears)?a():b()})()}; \ No newline at end of file +var SN={C:{I:{CounterBlackout:false,MaxLength:140,PatternUsername:/^[0-9a-zA-Z\-_.]*$/,HTTP20x30x:[200,201,202,203,204,205,206,300,301,302,303,304,305,306,307],NoticeFormMaster:null},S:{Disabled:"disabled",Warning:"warning",Error:"error",Success:"success",Processing:"processing",CommandResult:"command_result",FormNotice:"form_notice",NoticeDataGeo:"notice_data-geo",NoticeDataGeoCookie:"NoticeDataGeo",NoticeDataGeoSelected:"notice_data-geo_selected",StatusNetInstance:"StatusNetInstance"}},messages:{},msg:function(a){if(typeof SN.messages[a]=="undefined"){return"["+a+"]"}else{return SN.messages[a]}},U:{FormNoticeEnhancements:function(b){if(jQuery.data(b[0],"ElementData")===undefined){MaxLength=b.find(".count").text();if(typeof(MaxLength)=="undefined"){MaxLength=SN.C.I.MaxLength}jQuery.data(b[0],"ElementData",{MaxLength:MaxLength});SN.U.Counter(b);NDT=b.find(".notice_data-text:first");NDT.bind("keyup",function(c){SN.U.Counter(b)});var a=function(c){window.setTimeout(function(){SN.U.Counter(b)},50)};NDT.bind("cut",a).bind("paste",a)}else{b.find(".count").text(jQuery.data(b[0],"ElementData").MaxLength)}},Counter:function(d){SN.C.I.FormNoticeCurrent=d;var b=jQuery.data(d[0],"ElementData").MaxLength;if(b<=0){return}var c=b-SN.U.CharacterCount(d);var a=d.find(".count");if(c.toString()!=a.text()){if(!SN.C.I.CounterBlackout||c===0){if(a.text()!=String(c)){a.text(c)}if(c<0){d.addClass(SN.C.S.Warning)}else{d.removeClass(SN.C.S.Warning)}if(!SN.C.I.CounterBlackout){SN.C.I.CounterBlackout=true;SN.C.I.FormNoticeCurrent=d;window.setTimeout("SN.U.ClearCounterBlackout(SN.C.I.FormNoticeCurrent);",500)}}}},CharacterCount:function(a){return a.find(".notice_data-text:first").val().length},ClearCounterBlackout:function(a){SN.C.I.CounterBlackout=false;SN.U.Counter(a)},RewriteAjaxAction:function(a){if(document.location.protocol=="https:"&&a.substr(0,5)=="http:"){return a.replace(/^http:\/\/[^:\/]+/,"https://"+document.location.host)}else{return a}},FormXHR:function(a){$.ajax({type:"POST",dataType:"xml",url:SN.U.RewriteAjaxAction(a.attr("action")),data:a.serialize()+"&ajax=1",beforeSend:function(b){a.addClass(SN.C.S.Processing).find(".submit").addClass(SN.C.S.Disabled).attr(SN.C.S.Disabled,SN.C.S.Disabled)},error:function(c,d,b){alert(b||d)},success:function(b,c){if(typeof($("form",b)[0])!="undefined"){form_new=document._importNode($("form",b)[0],true);a.replaceWith(form_new)}else{a.replaceWith(document._importNode($("p",b)[0],true))}}})},FormNoticeXHR:function(b){SN.C.I.NoticeDataGeo={};b.append('');b.attr("action",SN.U.RewriteAjaxAction(b.attr("action")));var c=function(d,e){b.append($('

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