// ==UserScript== // @name [s4s] interface // @namespace s4s4s4s4s4s4s4s4s4s // @version 2.1 // @author le fun css man AKA Doctor Worse Than Hitler, kekero // @email doctorworsethanhitler@gmail.com // @description Lets you view the greenposts. // @match https://boards.4chan.org/s4s/thread/* // @match http://boards.4chan.org/s4s/thread/* // @connect funposting.online // @run-at document-start // @grant GM_xmlhttpRequest // @grant GM.xmlHttpRequest // @grant unsafeWindow // @icon data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNiAxNiI+PHBhdGggZD0iTTAgMEgxNlYxNkgwIiBmaWxsPSIjZGZkIi8+PHBhdGggZD0iTTMgNCA2IDFoNGwzIDN2OGwtMyAzSDZMMyAxMiIgZmlsbD0iZ3JlZW4iLz48cGF0aCBkPSJtNS41IDExLjVoLTJ2LTdoMnYtM2MtMyAwLTUgMi41LTUgNi41IDAgNCAyIDYuNSA1IDYuNXptNSAzYzMgMCA1LTIuNSA1LTYuNSAwLTQtMi02LjUtNS02LjV2M2gydjdoLTJ6bS00LTRoM0wxMCAyLjVINlptMCAzaDN2LTNoLTN6IiBmaWxsPSIjZmZmIiBzdHJva2U9ImdyZWVuIi8+PC9zdmc+ // @downloadURL none // ==/UserScript== if(query("#s4sinterface-css")){ throw "Multiple instances of [s4s] interface detected" } var threadId=location.pathname.match(/\/thread\/(\d+)/)[1] var weekdays=["Sun","Mon","Tue","Wed","Thu","Fri","Sat"] var postForm={} var lastCommentForm var updateLinks=new Set() if(typeof GM=="undefined"){ window.GM={ xmlHttpRequest:window.GM_xmlhttpRequest } } // Request green posts var serverurl="https://funposting.online/interface/" getGreenPosts() onPageLoad(_=>{ // Classic post form var nameField=query("#postForm input[name=name]") if(nameField){ var commentField=query("#postForm textarea") addCommentForm(commentField,1) var greenToggle=element( ["button#toggle",{ class:"greenToggle", title:"[s4s] Interface", onclick:event=>{ event.preventDefault() event.stopPropagation() showPostFormClassic() } },"!"] ).toggle var nameParent=nameField.parentNode nameParent.classList.add("nameFieldParent") insertBefore(greenToggle,nameField) }else{ // Thread is archived showPostFormClassic() } getUpdateLinks() }) // Native extension QR document.addEventListener("QRNativeDialogCreation",onQRCreated) if(unsafeWindow.Main){ onNativeextInit() }else{ document.addEventListener("4chanMainInit",onNativeextInit) } // 4chan-X QR integration document.addEventListener("QRDialogCreation",onQRXCreated) function onPageLoad(func){ if(document.readyState=="loading"){ addEventListener("DOMContentLoaded",func) }else{ func() } } // Request green posts function getGreenPosts(){ GM.xmlHttpRequest({ method:"get", url:serverurl+"get.php?thread="+threadId, onload:response=>{ if(response.status==200){ onPageLoad(_=>{ var postsObj=JSON.parse(response.responseText) var postsCount=Object.keys(postsObj).length if(postsCount){ var oldPosts=queryAll(".greenPostContainer") for(var i=0;i{ } }) } // Add a post to the proper position in the thread function addPost(aPost,currentPost){ if(!currentPost){ currentPost=query(".thread>.postContainer") } var numberless=aPost.options=="numberless" var afterNo=numberless?"XXXXXX":aPost.after_no var postId=afterNo+"-"+aPost.id var date=new Date(aPost.timestamp*1000) var dateString= padding(date.getDate(),2)+"/"+ padding(date.getMonth()+1,2)+"/"+ (""+date.getFullYear()).slice(-2)+ "("+weekdays[date.getDay()]+")"+ padding(date.getHours(),2)+":"+ padding(date.getMinutes(),2)+":"+ padding(date.getSeconds(),2) var linkReply if(!numberless){ linkReply=[0, " ", ["a",{ href:"#p"+postId, title:"Link to this post" },"No."], ["a",{ href:"javascript:quote('"+postId+"');", onclick:insertQuote, title:"Reply to this post" },postId] ] } var post=element( ["div#post",{ class:"postContainer replyContainer greenPostContainer", id:"pc"+postId }, ["div",{ class:"sideArrows", id:"sa"+postId },">>"], ["div",{ class:"post reply", id:"p"+postId }, ["div",{ class:"postInfoM mobile", id:"pim"+postId }, ["span",{ class:"nameBlock" }, ["span",{ class:"name" },aPost.username], ["br"] ], ["span",{ class:"dateTime postNum", "data-utc":aPost.timestamp }, dateString, linkReply ] ], ["div",{ class:"postInfo desktop", id:"pi"+postId }, ["input",{ type:"checkbox", name:"ignore", value:"delete" }], ["span",{ class:"nameBlock" }, ["span",{ class:"name" },aPost.username] ], " ", ["span",{ class:"dateTime", "data-utc":aPost.timestamp },dateString], (!numberless&& ["span",{ class:"postNum desktop", onclick:insertQuote, title:"Reply to this post" },linkReply] ) ], ["blockquote",{ class:"postMessage", id:"m"+postId, innerHTML:aPost.text.replace(/\r/g,'') }] ] ] ).post // Add the post while(currentPost.nextSibling){ if(!/^pc\d+$/.test(currentPost.id)||currentPost.id.slice(2)<=aPost.after_no){ currentPost=currentPost.nextSibling }else{ return insertBefore(post,currentPost) } } return insertAfter(post,currentPost) } // Classic post form function showPostFormClassic(hide){ var formSelector="body>form:not(.greenPostForm)" var nameField=query(formSelector+" input[name=name]") var optionsField=query(formSelector+" input[name=email]") var commentField=query(formSelector+" textarea") if(hide){ if(postForm.classic){ if(nameField){ nameField.value=postForm.classic.name.value optionsField.value=postForm.classic.options.value commentField.value=postForm.classic.comment.value lastCommentForm=commentField } removeChild(postForm.classic.form) postForm.classic=0 } return } if(postForm.classic){ return } var username="" if(nameField){ username=nameField.value }else{ var nameMatch=document.cookie.match(/4chan_name=(.*?)(?:;|$)/) if(nameMatch){ username=nameMatch[1] } } postForm.classic=element( ["form#form",{ name:"post", action:serverurl+"post.php", method:"post", enctype:"multipart/form-data", class:"greenPostForm", onsubmit:submitGreenPost }, ["input",{ name:"thread", value:threadId, type:"hidden" }], ["table",{ class:"postForm" }, ["tbody", ["tr", ["td","Name"], ["td",{ class:"nameFieldParent" }, (nameField&& ["button#toggle",{ class:"greenToggle pressed", title:"[s4s] Interface", onclick:event=>{ event.preventDefault() event.stopPropagation() showPostFormClassic(1) } },"!"] ), ["input#name",{ type:"text", name:"username", tabIndex:1, placeholder:"Anonymous", value:username }] ] ], ["tr", ["td","Options"], ["td", ["input#options",{ type:"text", name:"options", tabIndex:2, value:optionsField?optionsField.value:"" }], ["input",{ type:"submit", tabIndex:6, value:"Post" }] ] ], ["tr", ["td","Comment"], ["td", ["textarea#comment",{ name:"text", tabindex:4, cols:48, rows:4, wrap:"soft", value:commentField?commentField.value:"" }] ] ] ] ] ] ) addCommentForm(postForm.classic.comment) originalForm=query("#postForm") if(originalForm){ originalForm=originalForm.parentNode }else{ originalForm=query("body>.closed+*") if(!originalForm){ originalForm=query("#op") } } insertBefore(postForm.classic.form,originalForm) } // Native extension quick reply function onNativeextInit(){ getUpdateLinks() unsafeWindow.QR.showInterface=unsafeWindow.QR.show var newQRshow=function(){ var event=document.createEvent("Event") event.initEvent("QRNativeDialogCreation",false,false) document.dispatchEvent(event) } if(typeof exportFunction=="function"){ newQRshow=exportFunction(newQRshow,document.defaultView) } unsafeWindow.QR.show=newQRshow } function onQRCreated(){ try{ unsafeWindow.QR.showInterface(threadId) }catch(e){} // Clean up post form if it was initialised before var oldToggle=query("#quickReply form:not(.greenPostForm) .greenToggle") if(oldToggle){ removeChild(oldToggle) } var oldPostForm=query("#quickReply form:not(.greenPostForm)") if(oldPostForm){ showPostFormQR(1) } var formSelector="#qrForm" var nameField=query(formSelector+" input[name=name]") nameField.value=query("#postForm input[name=name]").value nameField.tabIndex=0 var commentField=query(formSelector+" textarea") addCommentForm(commentField) var toggle=element( ["button#toggle",{ type:"button", class:"greenToggle", title:"[s4s] Interface", onclick:event=>{ event.preventDefault() event.stopPropagation() showPostFormQR() } },"!"] ).toggle var nameParent=nameField.parentNode nameParent.classList.add("nameFieldParent") insertBefore(toggle,nameField) } function showPostFormQR(hide){ var formSelector="#qrForm" var nameField=query(formSelector+" input[name=name]") var optionsField=query(formSelector+" input[name=email]") var commentField=query(formSelector+" textarea") if(hide){ if(postForm.QR){ nameField.value=postForm.QR.name.value optionsField.value=postForm.QR.options.value commentField.value=postForm.QR.comment.value lastCommentForm=commentField removeChild(postForm.QR.form) postForm.QR=0 } return } var qr=query("#quickReply form:not(.greenPostForm)") if(postForm.QR||!qr){ return } postForm.QR=element( ["form#form",{ name:"post", action:serverurl+"post.php", method:"post", enctype:"multipart/form-data", class:"greenPostForm", onsubmit:submitGreenPost }, ["input",{ name:"thread", value:threadId, type:"hidden" }], ["div",{ class:"nameFieldParent" }, ["button",{ type:"button", class:"greenToggle pressed", title:"[s4s] Interface", onclick:event=>{ showPostFormQR(1) } },"!"], ["input#name",{ type:"text", name:"username", class:"field", placeholder:"Anonymous", value:nameField.value }] ], ["div", ["input#options",{ type:"text", name:"options", class:"field", placeholder:"Options", value:optionsField.value }] ], ["div", ["textarea#comment",{ name:"text", class:"field", cols:48, rows:4, wrap:"soft", placeholder:"Comment", value:commentField.value }], ], ["div", ["span",{ class:"greenSubmit", onclick:event=>{ submitGreenPost(event,postForm.QR.form) } },"Post"] ] ] ) addCommentForm(postForm.QR.comment) insertBefore(postForm.QR.form,qr) } // 4chan-X QR function onQRXCreated(){ getUpdateLinks() var qrPersona=query("#qr .persona") if(!qrPersona){ return } var formSelector="#qr form:not(.greenPostForm)" var commentField=query(formSelector+" textarea") addCommentForm(commentField) var toggle=element( ["button#toggle",{ type:"button", class:"greenToggle", title:"[s4s] Interface", onclick:event=>{ event.preventDefault() event.stopPropagation() showPostFormQRX() } },"!"] ).toggle insertBefore(toggle,qrPersona.firstChild) } function showPostFormQRX(hide){ var formSelector="#qr form:not(.greenPostForm)" var nameField=query(formSelector+" input[name=name]") var optionsField=query(formSelector+" input[name=email]") var commentField=query(formSelector+" textarea") if(hide){ if(postForm.QRX){ nameField.value=postForm.QRX.name.value optionsField.value=postForm.QRX.options.value commentField.value=postForm.QRX.comment.value lastCommentForm=commentField removeChild(postForm.QRX.form) postForm.QRX=0 } return } var qrx=query(formSelector) if(postForm.QRX||!qrx){ return } postForm.QRX=element( ["form#form",{ name:"post", action:serverurl+"post.php", method:"post", enctype:"multipart/form-data", class:"greenPostForm", onsubmit:submitGreenPost }, ["input",{ name:"thread", value:threadId, type:"hidden" }], ["div",{ class:"persona" }, ["button",{ type:"button", class:"greenToggle pressed", title:"[s4s] Interface", onclick:event=>{ showPostFormQRX(1) } },"!"], ["input#name",{ name:"username", class:"field", placeholder:"Name", size:1, value:nameField.value }], ["input#options",{ name:"options", class:"field", placeholder:"Options", size:1, value:optionsField.value }] ], ["textarea#comment",{ name:"text", class:"field", placeholder:"Comment", value:commentField.value }], ["div",{ class:"file-n-submit" }, ["input",{ type:"submit", value:"Submit" }] ] ] ) addCommentForm(postForm.QRX.comment) insertBefore(postForm.QRX.form,qrx) } // Track last used comment field for inserting quotes function addCommentForm(commentField,notLast){ if(!notLast){ lastCommentForm=commentField } commentField.addEventListener("focus",event=>{ lastCommentForm=event.currentTarget }) } function insertQuote(event){ var commentField=lastCommentForm if(commentField&&document.contains(commentField)){ event.preventDefault() event.stopPropagation() var isQRX=commentField.closest("#qr") if(isQRX){ isQRX.hidden=0 } var text=">>"+event.currentTarget.firstChild.data+"\n" var caretPos=commentField.selectionStart commentField.value= commentField.value.slice(0,caretPos) +text +commentField.value.slice(commentField.selectionEnd) var range=caretPos+text.length commentField.setSelectionRange(range,range) commentField.focus() } } // Manually update thread with green posts function getUpdateLinks(){ var update=queryAll("[data-cmd=update],.updatelink>a") for(var i=0;i{ if(response.status==200){ if(/Post Successful/.test(response.responseText)){ form.getElementsByTagName("textarea")[0].value="" getGreenPosts() }else{ return postSubmitted(submit,response.status,response.responseText) } } postSubmitted(submit,response.status) }, onerror:response=>{ postSubmitted(submit) } }) } function postSubmitted(submit,errorCode,responseText){ if(submit.fakeButton){ submit.button.firstChild.data=submit.text submit.button.classList.remove("greenSubmitDisabled") }else{ submit.button.value=submit.text submit.button.disabled=0 } if(errorCode==200){ if(responseText){ alert("Could not submit post ("+responseText+")") } }else{ var alertText="Could not connect to the [s4s] interface" if(errorCode){ alertText+=" ("+errorCode+")" } alert(alertText) } } // Stylesheet var stylesheet=` .greenPostForm+form .postForm>tbody>tr:not(.rules), #quickReply .greenPostForm+form, #qr .greenPostForm+form{ display:none!important; } .greenPostForm .file-n-submit{ display:flex; align-items:stretch; justify-content:flex-end; height:25px; margin-top:1px; } .greenPostForm .file-n-submit input{ width:25%; background:linear-gradient(to bottom,#f8f8f8,#dcdcdc) no-repeat; border:1px solid #bbb; border-radius:2px; height:100%; } .greenPostContainer .post.reply{ background-color:#dfd!important; border:2px solid #008000!important; } .greenPostContainer .postMessage{ color:#000!important; } .greenToggle{ font-family:monospace; font-size:16px; line-height:17px; background:#ceb!important; width:24px; padding:0; border:1px solid #bbb; } .greenPostForm input:not([type=submit]), .greenPostForm textarea{ background-color:#dfd; color:#000; } .greenToggle.pressed{ background:#6d6!important; font-weight:bold; color:#fff; } .postForm .greenToggle+input{ width:220px!important; } .postForm .nameFieldParent, #quickReply .nameFieldParent{ display:flex; flex-direction:row; } .postForm textarea{ width:292px; } #quickReply .greenToggle{ width:23px; height:23px; } #quickReply .greenToggle+input{ width:273px!important; } .greenSubmit{ display:inline-block; width:75px; float:right; padding:1px 6px; text-align:center; border:1px solid #adadad; background-color:#e1e1e1; box-sizing:border-box; user-select:none; font:400 13.3333px Arial,sans-serif; font:-moz-button; color:#000; cursor:default; } .greenSubmit:hover{ border-color:#0078d7; background-color:#e5f1fb; } .greenSubmit:active{ border-color:#005499; background-color:#cce4f7; } .greenSubmitDisabled{ color:#808080; pointer-events:none; } @media only screen and (max-width:480px){ .postForm .greenToggle+input{ width:196px!important; } .postForm input[type="submit"]{ width:60px; padding:2px 4px 3px; margin:0; } .postForm:not(.hideMobile){ margin-top:20px; } } `.replace(/\n\s*/g,"") element( document.head||document.documentElement, ["style",{ id:"s4sinterface-css" },stylesheet] ) function padding(string,num){ return (""+string).padStart(num,0) } function query(selector){ return document.querySelector(selector) } function queryAll(selector){ return document.querySelectorAll(selector) } function insertBefore(newElement,targetElement){ return targetElement.parentNode.insertBefore(newElement,targetElement) } function insertAfter(newElement,targetElement){ var nextSibling=targetElement.nextSibling if(nextSibling){ return insertBefore(newElement,nextSibling) }else{ return targetElement.parentNode.appendChild(newElement) } } function removeChild(targetElement){ return targetElement.parentNode.removeChild(targetElement) } function element(){ var parent var lasttag var createdtag var toreturn={} for(var i=0;i