4chan X

By aeosynth Last update Nov 13, 2009 — Installed 3,034 times. Daily Installs: 14, 7, 16, 9, 1, 5, 19, 3, 3, 12, 7, 14, 20, 15, 7, 9, 20, 19, 9, 20, 41, 43, 47, 19, 26, 29, 37, 24, 13, 31, 60, 20

There are 39 previous versions of this script.

Add Syntax Highlighting (this will take a few seconds, probably freezing your browser while it works)

// ==UserScript==
// @name           4chan X
// @namespace      aeosynth
// @include        http://cgi.4chan.org/*
// @include        http://img.4chan.org/*
// @include        http://orz.4chan.org/*
// @include        http://zip.4chan.org/*
// @description    Lightweight, featureful alternative to the 4chan extension / fychan
// @version        0.20.1
// @copyright      2009, James Campos
// @license        GPL version 3 or any later version; http://www.gnu.org/copyleft/gpl.html
// ==/UserScript==

//TODO
// tw - bold when threads watched
// tw - manual label
// tw - show thread died
// tw - respawn/refresh
// try objects for movement, watched threads
// 'restart updating' button
// trim ()
// quick quote - highlighted text appended to quotes
// qr restore
// following a quote breaks watcher?
// update notification don't reset if page is too small to scroll
// only show 'new posts' if filter didn't hide them
// quick spoiler
// issues when below /b/ackwash

(function () {// <-- Opera wrapper

function inBefore (root, el) {
	root.parentNode.insertBefore(el, root)
}

function inAfter (root, el) {
	root.parentNode.insertBefore(el, root.nextSibling)
}

function remove (el) {
	el.parentNode.removeChild (el)
}

function tag (el) {
	return document.createElement (el)
}

function text (el) {
	return document.createTextNode (el)
}

function $ (selector, root) {
	if (!root) root = document.body
	return root.querySelector(selector)
}

function $$ (selector, root) {
	if (!root) root = document.body
	var result = root.querySelectorAll(selector)
	var a = []
	for (var i = 0, l = result.length; i < l; i++)
		a.push(result[i])
	return a
}

function x (xpath, root) {
	if (!root) root = document.body
	return document.evaluate(xpath, root, null, XPathResult.ANY_UNORDERED_NODE_TYPE, null).singleNodeValue
}

function X (xpath, root) {
	if (!root) root = document.body
	var result = document.evaluate(xpath, root, null, XPathResult.UNORDERED_NODE_ITERATOR_TYPE, null)
	var a = [], item
	while (item = result.iterateNext())
		a.push (item)
	return a
}


if (/tmp|bin|dat|nov/.test(window.location.hostname)) {//quick reply error checking
	var body = document.body
	var error = $('table b')
	if (error)
		GM_setValue('error', error.firstChild.textContent)
	else if (!/Updating page/.test(body.textContent))//nginx
		GM_setValue('error', body.textContent)
	else
		GM_setValue('error', '')
}

const form = x('./form')
if (!form)
	return
const pathname = window.location.pathname
const board = pathname.match(/\/\w+\//)[0]
const reply = /res/.test(pathname)
var threadHidden = GM_getValue(board + 'thread', '')
var watchedBoards = GM_getValue('watchedBoards', '')
var threadSpans = $$('span[id^=nothread]', form)


if (GM_getValue('Forced Anon')) {
	anonymize (form)
	form.addEventListener('DOMNodeInserted', function(e){ if (e.target.nodeName == 'TABLE') anonymize(e.target) }, true)
}

function anonymize (root) {
	var names = $$('.commentpostername, .postername', root)
	for (var i = 0, l = names.length; i < l; i++)
		names[i].innerHTML = 'Anonymous'
	var trips = $$('.postertrip', root)
	for (var i = 0, l = trips.length; i < l; i++)
		remove (trips[i])
}

var filesize = $$('form > span.filesize')
if (!reply && GM_getValue('Thread Navigating', true))
	for (var i = 0, l = filesize.length; i < l; i++) {
		var nav = tag ('span')
		nav.className = 'navlinks'
		var top = tag ('a')
		top.name = i
		top.href = '#' + (i ? i - 1 : 'navtop')
		top.textContent = '▲'
		var bot = tag ('a')
		if (i == l - 1) {
			bot.href = 'http://' + window.location.hostname + board + (pathname.match(/\d+$/) + 1) + '.html'
			bot.textContent = '▶'
		} else {
			bot.href = '#' + (i + 1)
			bot.textContent = '▼'
		}
		nav.appendChild(top)
		nav.appendChild(text(' '))
		nav.appendChild(bot)
		inBefore(filesize[i], nav)
	}

if (!reply && GM_getValue('Thread Hiding', true))
	for (var i = 0, item; item = filesize[i]; i++) {
		var div = tag ('div')
		div.id = x('./span[starts-with(@id, "nothread")]', form).id
		div.innerHTML = '<a style = "cursor: pointer" title = "Hide Thread">[ - ]</a> '
		div.firstChild.addEventListener('click', hideThread, true)
		inBefore (item, div)
		while (!item.clear) {//<br clear="left"/>
			div.appendChild(item)
			item = div.nextSibling
		}
		if (threadHidden.indexOf(div.id) != -1)
			hideThread (div)
	}

function hideThread (div) {
	if (this.nodeName) {
		div = this.parentNode
		threadHidden += div.id + '\n'
		GM_setValue(board + 'thread', threadHidden)
	}
	div.style.display = 'none'

	var a = tag ('a')
	a.style.cursor = 'pointer'
	a.textContent = '[ + ]'
	a.title = 'Show Thread'
	a.addEventListener('click', showThread, true)
	var span = tag ('span')
	span.appendChild(a)
	span.appendChild(text (' '))
	span.appendChild($('.filetitle', div).cloneNode(true))
	span.appendChild(text (' '))
	span.appendChild(text ($('blockquote', div).textContent.slice(0, 25)))
	if (!GM_getValue ('Show Stubs', true)) {
		div.nextSibling.style.display = 'none'
		div.nextSibling.nextSibling.style.display = 'none'
		var prev = div.previousSibling
		if (prev.nodeName == 'SPAN')//Thread nav
			prev.style.display = 'none'
		span.style.display = 'none'
	}
	inBefore (div, span)
}

function showThread () {
	var div = this.parentNode.nextSibling
	div.style.display = ''
	threadHidden = threadHidden.replace(div.id + '\n', '')
	GM_setValue(board + 'thread', threadHidden)
	remove (this.parentNode)
}

if (GM_getValue('Thread Watcher', true)) {
	var watcher = tag ('div')
	watcher.innerHTML = '\
<div>\
Watched Threads\
</div>\
<div>\
</div>'
	watcher.id = 'tw'
	watcher.className = 'reply'
	watcher.style.display = GM_getValue('watcherDisplay', '')
	var top = GM_getValue('tw_top', '0px')
	if (top)
		watcher.style.top = top
	else
		watcher.style.bottom = '0px'
	var left = GM_getValue('tw_left', '0px')
	if (left)
		watcher.style.left = left
	else
		watcher.style.right = '0px'
	watcher.firstChild.addEventListener('mousedown', startMove, true)

	var tw = tag ('a')
	tw.textContent = 'TW'
	tw.className = 'pointer'
	tw.addEventListener('click', twF, true)
	var navtop = $('#navtop')
	inBefore(navtop.lastChild, text(' / '))
	inBefore(navtop.lastChild, tw)

	var re = /(.+)\n(.+)/g
	var tempArray
	var matches = watchedBoards.match(/\/\w+\//g)
	for (i in matches) {
		var tempWatched = GM_getValue(matches[i] + 'watched', '')
		while (tempArray = re.exec(tempWatched))
			addToWatcher (tempArray[1], tempArray[2])
	}
	document.body.appendChild(watcher)

	var watched = GM_getValue (board + 'watched', '')
	var threadSpans = $$('span[id^=nothread]', form)
	for (var i = 0, el; el = threadSpans[i]; i++) {
		var watch = tag ('a')
		watch.textContent = watched.indexOf(el.id.match(/\d+/)[0]) > -1 ? 'Unwatch' : 'Watch'
		watch.addEventListener('click', watchThread, true)
		watch.className = 'pointer'
		inAfter(el, watch)
		inAfter(el, text(' '))
	}
}

function twF () {
	var watcherDisplay = watcher.style.display ? '' : 'none'
	GM_setValue ('watcherDisplay', watcherDisplay)
	watcher.style.display = watcherDisplay
}

function addToWatcher (url, txt) {
	var x = tag ('a')
	x.textContent = 'X'
	x.addEventListener('click', watchThread, true)
	var a = tag ('a')
	a.href = url
	a.textContent = txt
	var span = tag ('span')
	span.appendChild(x)
	span.appendChild(text(' '))
	span.appendChild(a)
	span.appendChild(tag ('br'))
	watcher.lastChild.appendChild(span)
}

function watchThread() {
	if (this.textContent == 'X')
		var url = this.nextSibling.nextSibling.href
	else {
		if (reply)
			url = window.location.href.match(/[^#]+/)[0]
		else
			url = x("preceding-sibling::span[1]/a[3]", this).href
	}
	if (this.textContent == 'Watch') {
		this.textContent = 'Unwatch'
		var txt = x ('preceding-sibling::span[@class="filetitle"]', this).textContent//First look for subject
		if (!txt) {//then OP's text
			txt = x ('following-sibling::blockquote', this).textContent
			if (txt)
				txt = txt.slice(0,25)
			else {// finally OP's name/trip
				txt = x ('preceding-sibling::span[@class="postername"]', this).textContent
				var temp = x ('preceding-sibling::span[@class="postertrip"]', this)
				if (temp)
					txt += temp.textContent
			}
		}
		addToWatcher (url, txt)
		var watched = GM_getValue (board + 'watched', '')
		watched += url + '\n' + txt + '\n\n'
		GM_setValue(board + 'watched', watched)
		if (watchedBoards.indexOf(board) == -1)
			GM_setValue('watchedBoards', watchedBoards + board)
	} else {
		var tempBoard = url.match(/\/\w+\//)[0]
		if (this.textContent == 'Unwatch')
			this.textContent = 'Watch'
		else if (tempBoard == board) {// X button pressed, Unwatch button may or may not be present
			var watch = $('span#nothread' + url.match(/\/(\d+)/)[1] + ' + a', form)
			if (watch)
				watch.textContent = 'Watch'
		}
		var tempWatched = GM_getValue(tempBoard + 'watched', '')
		var re = new RegExp(url + '\n.+\n\n')
		tempWatched = tempWatched.replace(re, '')
		if (tempWatched)
			GM_setValue(tempBoard + 'watched', tempWatched)
		else {
			GM_deleteValue (tempBoard + 'watched')
			watchedBoards = watchedBoards.replace (tempBoard, '')
			GM_setValue ('watchedBoards', watchedBoards)
		}
		Array.forEach($$('a', watcher), function(el){ if(el.href == url) remove(el.parentNode) })
	}
}

if (GM_getValue('Thread Expansion', true)) {
	var omitted = $$('.omittedposts', form)
	for(var i = 0, omit; omit = omitted[i]; i++){
		var plus = tag ('a')
		plus.title = 'Load omitted posts'
		plus.textContent = '+'
		plus.className = 'load'
		plus.addEventListener('click', expandThread, true)
		inBefore(omit, plus)
		inBefore(omit, text(' '))
	}
}

function expandThread () {
	var text = this.nextSibling
	if (this.textContent == 'x') {
		text.textContent = ' '
		this.textContent = '+'
		return this.title = 'Expand Thread'
	}
	var omitted = text.nextSibling
	var l = parseInt(omitted.textContent)//XXX what about deleted posts? YOU SHUT YOUR WHORE MOUTH
	if (this.textContent == '-') {
		for (var i = 0; i < l; i++)
			remove (omitted.nextElementSibling)
		this.textContent = '+'
		return this.title = 'Expand Thread'
	}
	text.textContent = ' Loading... '
	this.textContent = 'x'
	this.title = 'Cancel Expansion'
	var url = x("preceding-sibling::span[starts-with(@id, 'nothread')]/a[contains(text(), 'Reply')]", this).href
	xThread(text, url)
}

function xThread (text, url) {
	var r = new XMLHttpRequest ()
	r.onreadystatechange = function() {
		if (this.readyState == 4 && text.textContent == ' Loading... ') {
			if (this.status == 200) {
				text.textContent = ' '
				text.previousSibling.textContent = '-'
				text.previousSibling.title = 'Contract Thread'
				const newPosts = this.responseText.match(/<a name=.*?>\n<table>[^]*?<\/table>/g)
				const first = x("following-sibling::a", text)
				var l = parseInt(text.nextSibling.textContent)
				for (var i = 0; i < l; i++) {
					var table = tag ('table')
					table.innerHTML = newPosts[i].match(/<table>([^]*?)<\/table>/)[1]
					inBefore(first, table)
				}
			} else {
				text.textContent = ' ' + this.status + ' ' + this.statusText + ' '
				text.previousSibling.textContent = '+'
				text.previousSibling.title = 'Expand Thread'
			}
		}
	}
	r.open('GET', url, true)
	r.send(null)
}

if (GM_getValue('Post Navigating', true)) {
	addThreadStartButton (form)
	form.addEventListener('DOMNodeInserted', function(e){ if (e.target.nodeName == 'TABLE') addThreadStartButton(e.target) }, true)
}

function addThreadStartButton (root) {
	var spans = $$('[id^=norep]', root)
	for (var i = 0, el; el = spans[i]; i++) {
		var bottom = tag ('a')
		bottom.textContent = '▼'
		bottom.style.cursor = 'pointer'
		bottom.addEventListener('click',
			function () {
				var temp = x("following::span[starts-with(@id, 'nothread')][1]", this)
				window.location = '#' + (temp ? temp.id : 'navbot')
			},
			true)
		inAfter(el, bottom)
		inAfter(el, text(' '))

		var top = tag ('a')
		top.textContent = '▲'
		top.style.cursor = 'pointer'
		top.addEventListener('click',
			function () { window.location = '#' + x("preceding::span[starts-with(@id, 'nothread')][1]", this).id },
			true)
		inAfter(el, top)
		inAfter(el, text(' '))
	}
}

if (GM_getValue('Report Button')) {
	addReportButton (form)
	form.addEventListener('DOMNodeInserted', function(e){ if (e.target.nodeName == 'TABLE') addReportButton(e.target) }, true)
}

function addReportButton (root) {
	var no = $$('span[id^=no]', root)
	for (var i = 0, l = no.length; i < l; i++) {
		var report = tag ('a')
		report.className = 'pointer'
		report.textContent = '[ ! ]'
		report.addEventListener('click', reportF, true)
		inAfter (no[i], report)
		inAfter (no[i], text (' '))
	}
}

function reportF () {
	x('preceding-sibling::input', this).click ()
	$('.deletebuttons input[type=button]').click ()
}
if (GM_getValue('Post Expansion', true)) {
	var abbr = $$('.abbr a')
	for (var i = 0, el; el = abbr[i]; i++)
		el.addEventListener('click', xPost, true)
}

function xPost (e) {
	e.preventDefault()
	var el = this.parentNode
	el.textContent = 'Loading...'
	var url = this.href

	var r = new XMLHttpRequest ()
	r.onreadystatechange = function() {
		if (this.readyState == 4){
			if(this.status == 200) {
				var id = url.match(/\d+$/)[0]
				var re = new RegExp('<a.*?name="' + id + '[^]*?<blockquote>([^]*?)<\/blockquote>', '')
				el.parentNode.innerHTML = this.responseText.match(re)[1]
			} else
				el.textContent = this.status + ' ' + this.statusText
		}
	}
	r.open('GET', url, true)
	r.send(null)
}

if (GM_getValue('Quick Reply', true)) {
	var iframe = tag ('iframe')
	iframe.name = 'iframe'
	var iframeReturn = false
	iframe.style.display = 'none'
	document.body.appendChild(iframe)
	iframe.addEventListener('load', doIframe, true)
	addQR(document.body)
	form.addEventListener('DOMNodeInserted', function(e){ if (e.target.nodeName == 'TABLE') addQR(e.target) }, true)
	if (reply && GM_getValue('Quick Post'))
		$('.postarea > form').target = 'iframe'
}

function doIframe () {
	if (iframeReturn = !iframeReturn)
		return//stop infinite loop when loading about:blank
	var qr = $('#qr')
	if (qr) {
		var error = GM_getValue('error', '')
		if (error) {
			qr.lastChild.style.visibility = ''
			var span = tag('span')
			span.textContent = error
			span.className = 'error'
			qr.appendChild(span)
		} else
			remove (qr)
	}
	iframe.src = 'about:blank'
}

function addQR (el) {
	var quotelinks = $$('span[id^=no] > a:nth-child(2)', el)
	for (var i = 0, l = quotelinks.length; i < l; i++)
		quotelinks[i].addEventListener('click', qr, true)
}

function qr (e) {//this could also be wrapped up in an object
	e.preventDefault()
	var qr = $('#qr')
	if (!qr) {
		qr = tag ('div')
		qr.id = 'qr'
		qr.className = 'reply'
		var top = GM_getValue('qr_top', '0px')
		if (top)
			qr.style.top = top
		else
			qr.style.bottom = '0px'
		var left = GM_getValue('qr_left', '0px')
		if (left)
			qr.style.left = left
		else
			qr.style.right = '0px'
		qr.innerHTML = '\
<div style="cursor: move">\
Quick Reply \
</div>'
		qr.firstChild.addEventListener('mousedown', startMove, true)
		var qr_shade = tag ('a')
		qr_shade.textContent = '_'
		qr_shade.addEventListener('click', function () {
			var qr_form = this.parentNode.nextSibling
			qr_form.style.visibility = qr_form.style.visibility ? '' : 'collapse'
			},
			true)
		var qr_close = tag ('a')
		qr_close.textContent = 'X'
		qr_close.addEventListener('click', function () {remove(qr)}, true)
		qr.firstChild.appendChild(qr_shade)
		qr.firstChild.appendChild(text(' '))
		qr.firstChild.appendChild(qr_close)

		var qr_form = $('.postarea > form').cloneNode(true)
		remove ($('tr:last-child', qr_form))
		qr_form.target = 'iframe'
		if (!reply) {
			var input = tag ('input')
			input.name = 'resto'
			input.type = 'hidden'
			var id = this.parentNode.id
			if (!/thread/.test(id))
				id = x('preceding::span[starts-with(@id, "nothread")][1]', this).id
			input.value = id.match(/\d+$/)[0]
			qr_form.appendChild(input)
		}
		qr_form.addEventListener('submit',
			function () {
				this.style.visibility = 'collapse'
				var span = this.nextSibling
				if (span)
					remove(span)
			}, true)
		qr.appendChild (qr_form)
		document.body.appendChild(qr)
	}
	var textArea = $('textArea', qr)
	var selection = window.getSelection()
	var selAnchor = selection.anchorNode
	if (selAnchor && selAnchor.parentNode.parentNode == this.parentNode.parentNode)
		var selText = selection.toString()
	textArea.value += '>>' + this.textContent.match(/\d+/) + '\n' + (selText ? '>' + selText + '\n' : '')
	textArea.scrollTop = textArea.scrollHeight
	textArea.focus()
}

const update_interval = GM_getValue('Interval', 30) * 1000
var last_modified = new Date(document.lastModified).toUTCString()
var pendingRequest = false
if (reply) {
	var replies = $$('td.reply')
	var fav = tag('link')
	fav.type = 'image/x-icon'
	fav.rel = 'shortuct icon'
	fav.href = '/favicon.ico'
	$('head', document).appendChild(fav)
	var threadDied = false
	window.addEventListener('scroll',
		function() {
			if (window.scrollY > window.scrollMaxY - 100)
				favicon()
		},
		true)
	if (GM_getValue('Show Updater', true) || GM_getValue('Auto-start Updater')) {
		document.title = '(0) ' + document.title
		scroll()
		window.addEventListener('scroll', scroll, true)
	}
	if(GM_getValue('Show Updater', true)) {
		var updater = tag('div')
		updater.id = 'updater'
		var left = GM_getValue('updater_left')
		if (left)
			updater.style.left = left
		else
			updater.style.right = '0px'
		var top = GM_getValue('updater_top')
		if (top)
			updater.style.top = top
		else
			updater.style.bottom = '0px'
		updater.className = 'reply'
		var updaterC = tag('div')//updaterChild. my move function requires it.
		updater.appendChild(updaterC)
		if (GM_getValue('Auto-start Updater')) {
			updaterC.textContent = 'On'
			var int = setTimeout(xUpdate, update_interval)
		} else
			updaterC.textContent = 'Off'
		updaterC.addEventListener('click', toggleUpdating, true)
		updaterC.addEventListener('mousedown', startMove, true)
		document.body.appendChild(updater)
	} else if (GM_getValue('Auto-start Updater'))
		int = setTimeout(xUpdate, update_interval)
}

function favicon (state) {
	if (!GM_getValue('Update Favicon', true))
		return
	var newFav = fav.cloneNode(true)
	switch (state) {
		case 'new'://halo
			newFav.href = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAgMAAABinRfyAAAADFBMVEUAAABmzDP///8AAABet0i+AAAAAXRSTlMAQObYZgAAAExJREFUeF4tyrENgDAMAMFXKuQswQLBG3mOlBnFS1gwDfIYLpEivvjq2MlqjmYvYg5jWEzCwtDSQlwcXKCVLrpFbvLvvSf9uZJ2HusDtJAY7Tkn1oYAAAAASUVORK5CYII='
			break
		case 'dead'://new posts ? red halo : red
			newFav.href = replies.length ? 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAWUlEQVR4XrWSAQoAIAgD/f+njSApsTqjGoTQ5oGWPJMOOs60CzsWwIwz1I4PUIYh+WYEMGQ6I/txw91kP4oA9BdwhKp1My4xQq6e8Q9ANgDJjOErewFiNesV2uGSfGv1/HYAAAAASUVORK5CYII='
				: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAgMAAABinRfyAAAACVBMVEUAAAAAAAD/AAA9+90tAAAAAXRSTlMAQObYZgAAADtJREFUCB0FwUERxEAIALDszMG730PNSkBEBSECoU0AEPe0mly5NWprRUcDQAdn68qtkVsj3/84z++CD5u7CsnoBJoaAAAAAElFTkSuQmCC'
			threadDied = true
			break
		default://threadDied ? red : default
			newFav.href = threadDied ? 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAgMAAABinRfyAAAACVBMVEUAAAAAAAD/AAA9+90tAAAAAXRSTlMAQObYZgAAADtJREFUCB0FwUERxEAIALDszMG730PNSkBEBSECoU0AEPe0mly5NWprRUcDQAdn68qtkVsj3/84z++CD5u7CsnoBJoaAAAAAElFTkSuQmCC'
				: '/favicon.ico'
	}
	fav.parentNode.replaceChild(newFav, fav)
	fav = newFav
}

function toggleUpdating() {
	if (this.textContent != 'Off') {
		this.textContent = 'Off'
		clearInterval(int)
	} else {
		this.textContent = 'On'
		if (!(pendingRequest))
			int = setTimeout(xUpdate, update_interval)
	}
}

function xUpdate () {
	pendingRequest = true
	if (updaterC)
		updaterC.textContent = 'Updating...'
	var r = new XMLHttpRequest ()
	r.open ('GET', pathname)
	r.onreadystatechange = function () {
		if (this.readyState == 4) {
			switch (this.status) {// 200 = ok, 304 = not modified, other = ;_;
				case 200:
					last_modified = this.getResponseHeader('Last-Modified')
					const newPosts = this.responseText.match(/<table>[^]*?<\/table>/g)
					var lastOld = x('.//br[@clear]/preceding-sibling::*[1]')
					var oldId = lastOld.nodeName == 'TABLE' ? $('.reply, .replyhl', lastOld).id : 0
					var i = newPosts.length - 1
					var newPost = newPosts[i]
					if (oldId < newPost.match(/\d+/)[0]) {
						favicon('new')
						do {
							var table = tag ('table')
							table.innerHTML = newPost.match(/<table>([^]*?)<\/table>/)[1]
							inAfter (lastOld, table)
							replies.push(table)
							i--
						} while ((newPost = newPosts[i]) && (oldId < newPost.match(/\d+/)[0]))
					}
					document.title = document.title.replace(/\d+/, replies.length)
				case 304:
					if (!updaterC || updaterC.textContent != 'Off')
						int = setTimeout(xUpdate, update_interval)
					break
				default:
					try {// thread died
						document.title = '(' + replies.length + ') ' + board + ' - ' + this.status + ' ' + this.statusText
						//document.title = (/^\* /.test(document.title) ? '* ':'') + board + ' - ' + this.status + ' ' + this.statusText
						var inputs = $$('.postarea > form input[type=submit]')
						for (var i = 0, l = inputs.length; i < l; i++)
							inputs[i].disabled = 'disabled'
						favicon('dead')
					} catch (e) {// connection error
						if (!updaterC || updaterC.textContent != 'Off')
							int = setTimeout(xUpdate, update_interval)
					}
			}
			pendingRequest = false
			if (updaterC)
				updaterC.textContent = this.status + ' ' + this.statusText
		}
	}
	r.setRequestHeader ('If-Modified-Since', last_modified)
	r.send ()
}

function scroll () {
	var height = document.body.clientHeight
	var unread = false
	for (var i in replies)
		if (replies[i].getBoundingClientRect().top > height - 50) {
			unread = true
			break
		}
	if (unread)
		replies = replies.splice(i)
	else
		replies = []
	document.title = document.title.replace(/\d+/, replies.length)
}

GM_registerMenuCommand('4chan X Options', options)
function options () {
	var div = tag ('div')
	div.id = 'options'
	div.className = 'reply'
	var top = GM_getValue('options_top', document.body.clientHeight / 2)
	if (top)
		div.style.top = top
	else
		div.style.bottom = '0px'
	var left = GM_getValue('options_left', document.body.clientWidth / 2)
	if (left)
		div.style.left = left
	else
		div.style.right = '0px'
	div.innerHTML = '\
<div>4chan X</div>\
<div>\
<label>Forced Anon<input type = "checkbox"></label><br>\
<label>Report Button<input type = "checkbox"></label><br>\
<label>Quick Post<input type = "checkbox"></label><br>\
<label>Quick Reply<input type = "checkbox" checked="true"></label><br>\
<label>Show Stubs<input type = "checkbox" checked="true"></label><br>\
<label>Post Expansion<input type = "checkbox" checked="true"></label><br>\
<label>Post Navigating<input type = "checkbox" checked="true"></label><br>\
<label>Thread Watcher<input type = "checkbox" checked="true"></label><br>\
<label>Thread Hiding<input type="checkbox" checked="true"></label><br>\
<label>Thread Expansion<input type = "checkbox" checked="true"></label><br>\
<label>Thread Navigating<input type = "checkbox" checked="true"></label><br>\
<label>Show Updater<input type = "checkbox" checked="true"></label><br>\
<label>Auto-start Updater<input type = "checkbox"></label><br>\
<label>Update Favicon<input type = "checkbox" checked="true"></label><br>\
Interval (s)<input size = 3 maxlength = 5><br>\
<input type = "button" value = "Clear Hidden"><br>\
<a>save</a> <a>cancel</a>\
</div>'
	div.firstChild.addEventListener('mousedown', startMove, true)
	var temp = $$('input[type=checkbox]', div)
	for (var i = 0, l = temp.length; i < l; i++)
		temp[i].checked = GM_getValue(temp[i].parentNode.textContent, temp[i].checked)
	temp = $('input[size]', div)
	temp.value = GM_getValue('Interval', 30)
	temp = $('input[type=button]', div)
	temp.addEventListener('click',
		function () {
			GM_deleteValue (board + 'thread')
			GM_deleteValue (board + 'reply')
		},
		true)
	temp = $$('a', div)
	temp[0].addEventListener('click', function () {//save
		var div = this.parentNode.parentNode
		var temp = $$('input[type=checkbox]', div)
		for (var i = 0, l = temp.length; i < l; i++)
			GM_setValue(temp[i].parentNode.textContent, temp[i].checked)
		temp = $('input[size]', div)
		if (!temp.value.length || isNaN(temp.value) || temp.value < 0)
			temp.value = 20
		GM_setValue('Interval', temp.value)
		remove (div)
		},
		true)
	temp[1].addEventListener('click', function () {//cancel
		var div = this.parentNode.parentNode
		remove (div)
		},
		true)
	document.body.appendChild(div)
}

var initial_mouseX
var initial_mouseY
var initial_boxX
var initial_boxY
var el
function startMove (event) {//TODO wrap this up in an object
	el = this.parentNode
	initial_mouseX = event.clientX
	initial_mouseY = event.clientY
	if (el.style.right)
		initial_boxX = 0
	else
		initial_boxX = document.body.clientWidth - el.offsetWidth - parseInt(el.style.left)
	if (el.style.bottom)
		initial_boxY = document.body.clientHeight - el.offsetHeight
	else
		initial_boxY = parseInt(el.style.top)
	document.addEventListener('mousemove', move, true)
	document.addEventListener('mouseup', endMove, true)
}

function move (event) {
	var right = initial_boxX + initial_mouseX - event.clientX
	var left = document.body.clientWidth - el.offsetWidth - right
	el.style.right = ''
	if (right < 15) {
		el.style.right = 0
		el.style.left = ''
	} else if (left < 15)
		el.style.left = 0
	else
		el.style.left = left + 'px'
	var top = initial_boxY - initial_mouseY + event.clientY
	var bottom = document.body.clientHeight - el.offsetHeight - top
	el.style.bottom = ''
	if (bottom < 15) {
		el.style.bottom = 0
		el.style.top = ''
	} else if (top < 15)
		el.style.top = 0
	else
		el.style.top = top + 'px'
}

function endMove () {
	document.removeEventListener('mousemove', move, true)
	document.removeEventListener('mouseup', endMove, true)
	GM_setValue(el.id + '_left', el.style.left)
	GM_setValue(el.id + '_top', el.style.top)
}

GM_addStyle ("\
.error { color: red; }\
.navlinks { position: absolute; right: 5px }\
.navlinks > a { text-decoration: none; font-size: 16px;}\
.load { font-size: 16px; cursor: pointer; font-weight: bold }\
.pointer { cursor: pointer; }\
#qr { position: fixed; border: 1px ridge; color: inherit; text-align: right; }\
#qr a { cursor: pointer; }\
#qr > span { position: absolute; bottom: 0; left: 0; }\
#options { position: fixed; border: 1px ridge; color: inherit; text-align: right; }\
#options a,\
#options label { cursor: pointer; }\
#options div:first-child { cursor: move; padding: 5px 0 0 0; text-align: center; text-decoration: underline; }\
#tw { position: absolute; border: 1px ridge; color: inherit; }\
#tw a { cursor: pointer; }\
#tw div:first-child { text-decoration: underline; }\
#tw div:first-child { cursor: move; padding: 5px 5px 0 5px; }\
#tw div:last-child,\
#options div:last-child { padding: 0 5px 5px 5px; }\
#updater { position:fixed; border: 1px solid; cursor:pointer; padding:10px; color:inherit; }\
#updater:active { cursor:move; }\
")

}) ()