[s4s] interface

Lets you view the greenposts.

As of 08.04.2018. See апошняя версія.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name        [s4s] interface
// @namespace   s4s4s4s4s4s4s4s4s4s
// @version     2.1
// @author      le fun css man AKA Doctor Worse Than Hitler, kekero
// @email       [email protected]
// @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+
// ==/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<oldPosts.length;i++){
							removeChild(oldPosts[i])
						}
						var currentPost
						for(var i=postsCount;i--;){
							currentPost=addPost(postsObj[i],currentPost)
						}
					}
				})
			}
		},
		onerror:response=>{
		}
	})
}

// 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<update.length;i++){
		if(!updateLinks.has(update[i])){
			update[i].addEventListener("click",getGreenPosts)
			updateLinks.add(update[i])
		}
	}
}

// Submit a green post
function submitGreenPost(event,form){
	event.preventDefault()
	event.stopPropagation()
	if(!form){
		form=event.currentTarget
	}
	var submit={}
	submit.button=form.querySelector(":scope input[type=submit],:scope .greenSubmit")
	submit.fakeButton=submit.button.classList.contains("greenSubmit")
	if(submit.fakeButton){
		submit.text=submit.button.firstChild.data
		submit.button.firstChild.data="..."
		submit.button.classList.add("greenSubmitDisabled")
	}else{
		submit.text=submit.button.value
		submit.button.value="..."
		submit.button.disabled=1
	}
	var data=[]
	var formData=new FormData(form)
	for(var nameValue of formData){
		data.push(
			nameValue[0]+"="
			+encodeURIComponent(nameValue[1].replace(/\r?\n/g,"\r"))
		)
	}
	data=data.join("&")
	GM.xmlHttpRequest({
		method:"post",
		headers:{
			"Content-type":"application/x-www-form-urlencoded"
		},
		url:serverurl+"post.php",
		data:data,
		onload:response=>{
			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<arguments.length;i++){
		var current=arguments[i]
		if(current){
			if(current.nodeType){
				parent=lasttag=current
			}else if(Array.isArray(current)){
				for(var j=0;j<current.length;j++){
					if(current[j]){
						if(!j&&typeof current[j]=="string"){
							var tagname=current[0].split("#")
							lasttag=createdtag=document.createElement(tagname[0])
							if(tagname[1]){
								toreturn[tagname[1]]=createdtag
							}
						}else if(current[j].constructor==Object){
							if(lasttag){
								for(var value in current[j]){
									if(value!="style"&&value in lasttag){
										lasttag[value]=current[j][value]
									}else{
										lasttag.setAttribute(value,current[j][value])
									}
								}
							}
						}else{
							var returned=element(lasttag,current[j])
							for(var k in returned){
								toreturn[k]=returned[k]
							}
						}
					}
				}
			}else if(current){
				createdtag=document.createTextNode(current)
			}
			if(parent&&createdtag){
				parent.appendChild(createdtag)
			}
			createdtag=0
		}
	}
	return toreturn
}