Hide Threads and Replies by Poster

Hides threads based on the player(s) that posted the thread or replied.

// ==UserScript==
// @name Hide Threads and Replies by Poster
// @namespace DanWL
// @version 3.1
// @description Hides threads based on the player(s) that posted the thread or replied.
// @author https://greasyfork.org/en/users/85040-dan-wl-danwl
// @match https://www.warzone.com/*
// @grant none
// ==/UserScript==
/* jshint esnext: false */
/* jshint esversion: 8 */
/* jshint browser: true */
/* jshint devel: true */
/*
menu:
ui-
Moved settings menu to [Your name] > Dan's Userscripts > Hide Threads And Replies By Poster and simplified it.
To modify block list, go to the menu and enter profile links. Put each link on a new line.
Can also use your Warzone block list. Click "Sync" to update which players are on your block list.
Can hide based on clan. Able to exclude (whitelist) members for the hiding. Click "Sync" to update which members are in the clan.
To hide threads based on their subject, go to the menu and enter either plain text or a JavaScript regular expression (regex). Each phase must be on a new line. By default, thread subjects are case-sensitive; you can override this by using a regex instead of plain text.

On thread pages:
Threads can be unhidden by clicking "Show X blocked threads". Once visible again, click "Hide X blocked threads" to hide them again.
NOT given add/remove thread/creator option because that's difficult to do in a fast, reliable way. it can be done via menu anyway. others have called the buttons "clunky" in the past

On reply pages:
Replies can be unhidden by clicking "- block listed post by xxx".
Use the "+/- Block list" button to alter the current block list.

On mail pages:
The participants are displayed

Discontinued thread exceptions due to no one having an understanding of what this does.
Rewrote the code from scratch.
Old settings exports (pre-2020) still work under the import button of this userscript.

user wants changes to apply instantly
user wants ability to import/export settings
user wants way to reset

cli (browser console)-
configDan {
	version: String
	blocklist {
		view async() @returns String of players on block list (player numbers)
		syncWithWZ async(keepList, keepCurrentBlocklist) @param keepList String Array of players to keep. must already be in block list else skipped, use [] to say none @param keepCurrentBlocklist Booleany use true to not overwrite
		lastBlocklistSync async() @returns String (ISO date)
		add async(playerLinks) @param playerLinks String Array of player links
		remove async(playerLinks) @param playerLinks String Array of player links
		overwrite async(playerLinks) @param playerLinks String Array of player links
		clans {
			add async(id) overwrites clan id if already hiding @param id clan id
			remove async(id) removes the whole clan from clan block list @param id clan id
			list async() @returns Object of {
				id: Int
				lastSync: String (ISO date)
				members: Int Array of which members are in the clan as of last sync (player numbers)
				keepList: String Array of which members are not having their content hidden (player numbers)
				sync async(keepList, preserveCurrentKeepList) @param keepList String Array of member profiles to keep block listed. Use [] to say none. By default, all new members of the clan will have their content hidden @param preserveCurrentKeepList Booleany use true to not overwrite
				overwriteKeepList async(keepList) @param keepList String Array of member profiles
			}
		}
	}
	threads {
		hideUsingStr async(str) @param str String to perform a case-sensitive match on. Use a new line to separate multiple phases. Overwrites current setting
		hideUsingRe async(re) @param re RegExp saying which threads to hide based on their subject matching the regex. Include any flags you wish to use inside the regex. Overwrites current re and flags setting
		useReMode async(useRe) @param useRe truthy or falsey. If truthy the re with flags is used. Else the string of phases is used
		currentlyHidingUsing async() @returns Object {str: String, inReMode: Boolean, re: String, flags: String}
	}
	settings {
		import async(imported) @param imported JSON String of previously exported storage
		export async() @returns JSON String
		reset()
	}
}

storage-
legacy-
localStorage.players was list of player numbers > move to players
localStorage.Players was list of player ids > convert to player numbers > move to players
localStorage.DanHTRBP_hidingOT - "1" means hide
localStorage.DanHTRBP_hide_blank_name - "1" means hide
localStorage._threads was list of threads to hide by name > convert to hideThreads.re
localStorage.MOTW was to hide map of the week > move to hideThreads.str
	discontinued-
	localStorage.DanHTRBP_hidingUI to hide buttons due to appearing "clunky"
	localStorage.DanHTRBP_hide_threads
	localStorage.DanHTRBP_hide_replies
	localStorage.DanHTRBP_version_changes
	localStorage.DanHTRBP_MailId
	localStorage.DanHTRBP_player_data
	localStorage.threads was thread exceptions
	localStorage.bpDialogue was to hide alerts saying blank threads/blank posts were hidden
	localStorage.dans_userscript_user
	localStorage.dans_userscripts -> invalid script name

current-
localStorage.dans_userscripts.USERSCRIPT_NAME {
	UPDATE_NO: Signed Int OR falsey -> use current - 1
	players: Int Array of player numbers
	lastBlocklistSync String (ISO date OR empty)
	hideThreads {
		str: String
		inReMode: Boolean
		re: String
		flags: String
	}
	hideOT true OR false
	hideBlankName true or false
	clans {
		clanId {
			// lastSync: String (ISO date)
			// members: Int Array
			keepList: Int Array of PLAYERNO
		}
	}
	pmParticipants {
		threadId: [] // of PLAYERNO
	}
}

localStorage.dans_userscripts.SHARED {
	clans {
		CLANID: {
			lastUpdate: String (ISO date) OR -1
			members: {
				PLAYERNO: 1
			}
		}
	}
	players {
		PLAYERNO {
			name: String 
			title: String
			lastSeen: String
			boot: int
			points: int
			clan: int
		}
	}
	threads {
		THREADID: {
			creatorId: PLAYERNO
			category: String
			name: String
		}
	}
	threadCategories: {
		CATEGORY: {
			THREADID: 1
		}
	}
}

TODO tidy up reminders from shared.clans
*/

(function() {
	const THIS_USERSCRIPT = {
		NAME: "hide_threads_and_replies_by_poster",
		VERSION: "3.1",
		UPDATE_NO: 3,
		VERSION_CHANGES: `<li>Displays PM participants on the mail page</li>`
	};

	function sleep(duration) {
		const main = new Promise((resolve, reject) => {
			setTimeout(() => {
				resolve();
			}, duration);
		});

		return main;
	}

	function fetchWithTimeout(url, timeout) {
		// https://stackoverflow.com/questions/46946380/fetch-api-request-timeout#answer-49857905
		if (timeout < 1 || typeof timeout != "number") {
			return;
		}

		return Promise.race([
			fetch(url),
			new Promise((_, reject) => {
				setTimeout(() => reject(new Error("timeout")), timeout);
			})
		]);
	}

	async function fetchText(url, delay) {
		if (typeof delay != "number") {
			delay = 1000;
		}

		await sleep(delay);

		// https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch
		// if there was a connection when it started but later goes after 10 secs, try again
		// but if there was none in the first place, waits until it comes back

		return await fetchWithTimeout(url, 10000)
			.then(response => {
				if (response.ok) {
					return response.text();
				}
				else {
					throw new Error("unable to load url " + url);
				}
			})
			.then(data => {
				return data;
			})
			.catch(err => {
				if (err.message == "timeout") {
					return fetchText(url, delay * 2);
				}

				throw err;
			});
	}

	const profileLinkRe = /^\s*(?:https?:\/\/)?(?:(?:www\.)?)?(?:(?:warzone\.com)|(?:warlight\.net))\/Profile\?p=(\d{8,})\s*$/i;
	// 5 because first 2 and last 2 are random. need at least 1 for an id

	const createMenuOptions = {
		mainContent: `<div id="ot">
				<input type="button">
			</div>
			<label for="action">Manage:</label>
			<select id="action">
				<option value="blocklist">Block list</option>
				<option value="threads">Thread hiding</option>
			</select>
			<div id="actionContent"></div>`,
		useCollapsible: true,
		setupMainContentEvents: function(content) {
			function tmpMsg(output, msg) {
				output.innerHTML = msg;
				sleep(3000).then(() => {
					output.innerHTML = '';
				});
			}

			const otBtn = content.querySelector('#ot input');
			async function setOTVal(hiding) {
				if (typeof hiding != 'boolean') {
					hiding = await getStorageItem('hideOT');
				}

				otBtn.value = (hiding ? 'Unhide' : 'Hide') + ' off-topic threads';
			}
			async function changeOTVal() {
				const hiding = !(await getStorageItem('hideOT'));

				await setStorageItem('hideOT', hiding);
				await setOTVal(hiding);
			}
			otBtn.onclick = changeOTVal;
			setOTVal();

			const select = content.querySelector("#action");
			const actionContent = content.querySelector("#actionContent");

			function mng() {
				actionContent.innerHTML = "";

				switch (select.value) {
					case "blocklist":
						doBlocklist();
						break;
					case "threads":
						doThreads();
						break;
				}
			}

			select.onchange = mng;
			mng();
			updateMenu = mng;

			function doBlocklist() {
				const profileLinksRe = new RegExp(profileLinkRe.source, "img");

				actionContent.innerHTML = `<select id="blocklistType">
					<option value="normal">View or change</option>
					<option value="syncBlocklist">Sync with your account's block list</option>
					<option value="clans">Clan block listing</option>
				</select>
				<div id="blocklistTypeContent"></div>`;

				const blocklistActions = {
					normal: async (content) => {
						content.innerHTML = `<p>Enter player links to block list (one per line).</p>
						<input type="button" value="Save">
						<div id="output"></div>
						<div>
							<textarea placeholder="${profileLinkEgs}" style="width: 100%; min-height: 260px;">${await listBlocklisted()}</textarea>
						</div>`;

						const output = content.querySelector("#output");

						content.querySelector("input").onclick = function() {
							output.innerHTML = "";

							const links = actionContent.querySelector("textarea").value.match(profileLinksRe) || [];

							config.blocklist.overwrite(links).then(() => {
								tmpMsg(output, "Saved");
								listBlocklisted();
							});
						};

						async function listBlocklisted() {
							const players = await getStorageItem("players");
							let blocklisted = "";

							players.forEach((id, i) => {
								blocklisted += "warzone.com/Profile?p=" + id;

								if (i + 1 < players.length) {
									blocklisted += "\n";
								}
							});

							return blocklisted;
						}
					},
					syncBlocklist: async (content) => {
						async function updateLastSyncDate() {
							let date = await config.blocklist.lastBlocklistSync();

							if (!date) {
								date = "never";
							}
							else {
								date = new Date(date).toLocaleString();
							}

							return date;
						}

						content.innerHTML = `<p>Last block list sync was done on <span id="date">${await updateLastSyncDate()}</span>. Are there any players that you want to keep block listed after syncing? One link per line.</p>
						<p>
							<label for="preserve">Keep current block list but add anyone else?</label>
							<input id="preserve" type="checkbox">
						</p>
						<input type="button" value="Sync" style="display: block;">
						<textarea placeholder="${profileLinkEgs}" style="width: 100%; min-height: 260px;"></textarea>`;

						const btn = content.querySelector("input[type='button']");

						btn.onclick = () => {
							btn.disabled = true;

							const links = content.querySelector("textarea").value.match(profileLinksRe) || [];

							config.blocklist.syncWithWZ(links, content.querySelector("#preserve").checked).then(async () => {
								content.querySelector('#date').innerText = await updateLastSyncDate();
								btn.disabled = false;
							});
						};
					},
					clans: async (content) => {
						let contentHTML = `<p>Select clan to manage&nbsp;
							<select>`;

						await config.blocklist.clans.list().then((list) => {
							for (let clanId in list) {
								contentHTML += `\n\t<option value="${clanId}">${clanId}</option>`;
							}
						});

						contentHTML += `\n</select>
						</p>
						<div id="manageClanArea"></div>
						<div id="addClanArea">
							<label for="clanId">Add clan number to block list:</label>
							<input id="clanId" type="number" min="0">&nbsp;
							<input id="addClan" type="button" value="Go">
							<div id="output"></div>
						</div>`;

						content.innerHTML = contentHTML;

						const clanManage = content.querySelector("select");

						async function updateClan() {
							const list = await config.blocklist.clans.list();
							const manageClanArea = content.querySelector("#manageClanArea");
							const clanId = parseInt(clanManage.value);

							manageClanArea.innerHTML = "";

							const clan = list[clanId];

							if (!clan) {
								return;
							}

							manageClanArea.innerHTML = `<p>Last sync was done on ${new Date(clan.lastSync).toLocaleString()}.&nbsp;<input id="prepareSync" type="button" value="Sync">&nbsp;<input class="deleteClanBtn" type="button" value="Remove clan"></p>
							<div id="prepareSyncArea" style="display: none;">
								In the space below, enter any profile links that you want to keep whitelisted when updating the clan member list. Afterwards, click "Go".</p>
								<input type="button" value="Go" style="display: block;">
								<p>
									<label for="preserve">Keep current whitelist?</label>
									<input id="preserve" type="checkbox">
								</p>
								<textarea placeholder="${profileLinkEgs}" style="width: 100%; min-height: 260px;"></textarea>
							</div>
							<p>
								<input id="toggleKeepList" type="button" value="Toggle member whitelisting">
								<div id="updateKeepListDesc">
									<label for="updateKeepList">Select players you wish to exclude from hiding.</label>
									<input id="updateKeepList" type="button" value="Update clan whitelist">
								</div>
							</p>`;

							const prepareSyncArea = manageClanArea.querySelector("#prepareSyncArea");
							const goBtn = prepareSyncArea.querySelector("input");
							const links = prepareSyncArea.querySelector("textarea").value.match(profileLinksRe) || [];
							const preserve = prepareSyncArea.querySelector("#preserve").checked;

							manageClanArea.querySelector("#prepareSync").onclick = function() {
								prepareSyncArea.style.display = "block";
							};

							const deleteClanBtn = manageClanArea.querySelector('.deleteClanBtn');

							deleteClanBtn.onclick = function() {
								config.blocklist.clans.remove(clanId, deleteClanBtn).then(() => {
									blocklistActions.clans(content);// update everything
								});
							};

							goBtn.onclick = function() {
								goBtn.disabled = true;
								clan.sync(links, preserve).then(() => {
									updateClan();
								});
							};

							const desc = manageClanArea.querySelector("#updateKeepListDesc");
							const updateKeepListItems = document.createElement('div');
							const toggleKeepListBtn = {
								hiding: true,
								get: function() {
									return manageClanArea.querySelector("#toggleKeepList");
								},
								update: function() {
									desc.style.display = this.hiding ? 'none' : 'block';

									this.hiding = !this.hiding;
								}
							};

							updateKeepListItems.style.maxHeight = '30em';// 15 players
							updateKeepListItems.style.overflow = 'auto';

							toggleKeepListBtn.get().onclick = toggleKeepListBtn.update;
							toggleKeepListBtn.update();

							let updateKeepListItemsHTML = '';

							clan.keepList.forEach((number) => {
								updateKeepListItemsHTML += `<p><a href="https://www.warzone.com/Profile?p=${number}">${number}</a>&nbsp;<input id="${number}" type="checkbox" checked="checked"></p>`;
							});
							getBlocklistedMembers(clan).forEach((number) => {
								updateKeepListItemsHTML += `<p><a href="https://www.warzone.com/Profile?p=${number}">${number}</a>&nbsp;<input type="checkbox"></p>`;
							});

							updateKeepListItems.innerHTML = updateKeepListItemsHTML;

							desc.appendChild(updateKeepListItems);

							const checkboxes = updateKeepListItems.querySelectorAll("input");
							const newKeepList = [];

							for (let i = 0; i < checkboxes.length; i++) {
								const checkbox = checkboxes[i];

								if (checkbox.checked) {
									newKeepList.push(checkbox.previousElementSibling.href);
								}
							}

							desc.querySelector("#updateKeepList").onclick = function() {
								clan.overwriteKeepList(newKeepList).then(() => {
									updateClan();
								});
							};
						}

						clanManage.onchange = updateClan;
						updateClan();

						const addClanArea = content.querySelector("#addClanArea");
						const clanIdInput = addClanArea.querySelector("#clanId");
						const addClanBtn = addClanArea.querySelector("#addClan");
						const output = addClanArea.querySelector("#output");

						addClanBtn.onclick = function() {
							addClanBtn.disabled = true;
							output.innerHTML = "";
							output.className = "";

							config.blocklist.clans.add(clanIdInput.value)
								.then(() => {
									blocklistActions.clans(content);// updates everything
								},
								(err) => {
									console.exception(err);// for non-custom errors

									output.className = "errors";
									output.innerHTML = err;
									addClanBtn.disabled = false;
								});
						};
					}
				};

				const blocklistType = actionContent.querySelector("#blocklistType");

				function mngBL() {
					const blocklistTypeContent = actionContent.querySelector("#blocklistTypeContent");

					blocklistTypeContent.innerHTML = "";
					blocklistActions[blocklistType.value](blocklistTypeContent);
				}

				blocklistType.onchange = mngBL;
				mngBL();
			}

			async function doThreads() {
				async function updateMode() {
					const modeName = actionContent.querySelector("#modeName");
					const modeContent = actionContent.querySelector("#modeContent");
					const hideThreads = await getStorageItem("hideThreads");

					if (hideThreads.inReMode) {
						modeName.innerHTML = '<abbr title="JavaScript">JS</abbr> regular expression';
						modeContent.innerHTML = `<p>Enter regex</p>
						<input id="done" type="button" value="Done">
						<p>/<input id="re" type="text" value="${hideThreads.re}" style="width: calc(100% - 5em);">/<input id="flags" type="text" value="${hideThreads.flags}" style="width: 4em;"></p>
						<div id="output"></div>`;

						const output = modeContent.querySelector("#output");

						modeContent.querySelector("#done").onclick = function() {
							const re = modeContent.querySelector("#re").value;
							const flags = modeContent.querySelector("#flags").value;

							output.className = "";
							output.innerHTML = "";

							try {
								config.threads.hideUsingRe(new RegExp(re, flags))
									.then(() => {
										tmpMsg(output, "This is valid :)");									
									},
									(err) => {
										output.className = "errors";
										output.innerHTML = err.message;
									});
							}
							catch(err) {
								output.className = "errors";
								output.innerHTML = err.message;
							}
						};
					}
					else {
						modeName.innerHTML = 'phases';
						modeContent.innerHTML = `<p>Enter phases. Use a new line to seperate phases.</p>
						<input id="done" type="button" value="Done">
						<div id="output"></div>
						<textarea>${hideThreads.str}</textarea>`;
						const output = modeContent.querySelector('#output');

						modeContent.querySelector("#done").onclick = function() {
							config.threads.hideUsingStr(modeContent.querySelector("textarea").value)
								.then(() => {
									tmpMsg(output, 'Saved');
								});
						};
					}
				}

				actionContent.innerHTML += `<p>Currently using <span id="modeName"></span> mode.&nbsp;<input type="button" value="Change"></p>
				<div id="modeContent"></div>`;

				actionContent.querySelector("input").onclick = function() {
					getStorageItem("hideThreads").then((hideThreads) => {
						config.threads.useReMode(!hideThreads.inReMode).then(() => {
							updateMode();
						});
					});
				};

				await updateMode();
			}
		}
	};

	let GLOBALS = {};

	async function getScript(name, windowFuncName) {
		if (typeof window[windowFuncName] != "function") {
			if (window["fetching" + windowFuncName]) {
				// only fetch the script once, wait until first done then resume
				await sleep(100);
				return getScript(name, windowFuncName);
			}
			else {
				window["fetching" + windowFuncName] = true;

				await fetchText("https://danwales.github.io/scripts/" + name, 1000, {cache: "no-store"})
					.then((text) => {
						// the latest version is always given - https://javascript.info/fetch-api
						// not using latest version can allow bugs to emerge
						const script = document.createElement("script");

						script.innerHTML = text;
						document.head.appendChild(script);
					});

				window["fetching" + windowFuncName] = false;
			}
		}

		return window[windowFuncName];
	}

	async function getCommonCode() {
		// initializes storage and creates dans userscripts menu as well as the settings menu for this userscript
		GLOBALS = await (await getScript("userscripts_common.js", "createDansUserscriptsCommon"))(THIS_USERSCRIPT, validateStorage, importLegacy, createMenuOptions);
		GLOBALS.extract = (await getScript("extract.js", "EXTRACT"));
	}

	async function validateStorage(is, GLOBALS) {
		return await new Promise((resolve, reject) => {
			const discontinued = ["DanHTRBP_hidingUI", "DanHTRBP_hide_threads", "DanHTRBP_hide_replies", "DanHTRBP_version_changes", "DanHTRBP_MailId", "DanHTRBP_player_data", "threads", "bpDialogue", "dans_userscript_user"];
			const legacy = ["Players", "players", "_threads", "MOTW", "DanHTRBP_hidingOT", "DanHTRBP_hide_blank_name"];
		
			let needsToUpdateStorage = false;// by nature if still got player ids, the id to number process take a significantly longer time compared to general validation

			function checkIfStorageLocationExists(locations) {
				if (needsToUpdateStorage) {
					return;
				}

				for (let i = 0; i < locations.length; i++) {
					if (localStorage[locations[i]] !== undefined) {
						needsToUpdateStorage = true;
						break;
					}
				}
			}

			checkIfStorageLocationExists(discontinued);
			checkIfStorageLocationExists(legacy);

			let storageProgressVisual;
			if (needsToUpdateStorage) {
				storageProgressVisual = GLOBALS.Alert(`<p id="msg">Updating ${THIS_USERSCRIPT.NAME} storage. Please wait...<p>`);
			}

			if (typeof is[THIS_USERSCRIPT.NAME] != "object" || Array.isArray(is[THIS_USERSCRIPT.NAME])) {
				is[THIS_USERSCRIPT.NAME] = {};
			}

			const tasks = [checkUpdateNumber, checkPlayerList, checkLastBlockListSync, checkThreadsToHide, checkHidingOT, checkHideBlankName, checkBlocklistedClans];
			const taskList = new GLOBALS.TaskList(tasks, finalise);
			const storageTV = new GLOBALS.TaskVisual(storageProgressVisual);

			taskList.setOnTaskStart(taskName => {
				storageTV.setProgress(taskName, 'Started');
			});
			taskList.setOnTaskCompletion(taskName => {
				storageTV.setProgress(taskName, 'Done');
			});

			storageTV.setup(tasks.concat(finalise));// doesn't change tasks (intentional)

			const validStorageLocationsForThisUserscript = ["UPDATE_NO", "players", "lastBlocklistSync", "hideThreads", "hideOT", "hideBlankName", "clans"];

			async function checkUpdateNumber() {
				if (typeof is[THIS_USERSCRIPT.NAME].UPDATE_NO != "number" || is[THIS_USERSCRIPT.NAME].UPDATE_NO < 1) {
					is[THIS_USERSCRIPT.NAME].UPDATE_NO = THIS_USERSCRIPT.UPDATE_NO - 1;
				}

				if (is[THIS_USERSCRIPT.NAME].UPDATE_NO > THIS_USERSCRIPT.UPDATE_NO) {
					is[THIS_USERSCRIPT.NAME].UPDATE_NO = THIS_USERSCRIPT.UPDATE_NO;
				}
			}

			async function checkPlayerList() {
				// await playerTaskList.run(); doesn't work because goes past once all started to run
				await new Promise((res, rej) => {
					const playersTasks = [checkForLegacyPlayerNumbers, checkForLegacyPlayerIds];
					const playerTaskList = new GLOBALS.TaskList(playersTasks, checkCurrentPlayers);
					const playersTV = new GLOBALS.TaskVisual(storageTV.find('#checkPlayerList'));

					playersTV.setup(playersTasks.concat(checkCurrentPlayers));// doesn't change tasks (intentional)

					playerTaskList.setOnTaskStart(taskName => {
						playersTV.setProgress(taskName, 'Started');
					});
					playerTaskList.setOnTaskCompletion(taskName => {
						playersTV.setProgress(taskName, 'Done');
					});

					async function checkForLegacyPlayerNumbers() {
						if (!localStorage.players) {
							return [];
						}

						return localStorage.players.split(/\,+/);
					}

					async function checkForLegacyPlayerIds() {
						async function convertIdToNumber(id) {
							return await fetchText("https://www.warzone.com/Discussion/Notes?p=" + id, 10).then((text) => {
								const playerNumber = text.match(/<a href="\/Profile\?p=(\d+)">&lt;&lt; Back to Profile<\/a>/);

								if (playerNumber) {
									return parseInt(playerNumber[1]);
								}
							});
						}
						async function convertIdsToNumbers(ids) {
							const size = ids.length;
							const firstPlayerEver = 4532;
							const numbers = [];

							for (let i = size - 1; i > -1; i--) {
								const id = parseInt(ids.pop());

								if (id >= firstPlayerEver && isFinite(id)) {
									const num = await convertIdToNumber(id);

									if (typeof num == "number") {
										numbers.push(num);
									}

									playersTV.setProgress('checkForLegacyPlayerIds', size - i + '/' + size);
								}
							}

							return numbers;
						}

						if (typeof localStorage.Players != "string") {
							return [];
						}

						const ids = localStorage.Players.split(/\,+/);

						playersTV.setProgress('checkForLegacyPlayerIds', '0/' + ids.length);

						return await convertIdsToNumbers(ids);
					}

					async function checkCurrentPlayers(returnValues) {
						// removes any duplicates and otherwise invalid
						let players = is[THIS_USERSCRIPT.NAME].players;

						if (!Array.isArray(players)) {
							players = [];
						}

						for (let tName in returnValues) {
							players = players.concat(returnValues[tName]);
						}

						const size = players.length;
						playersTV.setProgress('checkCurrentPlayers', '0/' + size);

						const valid = [];
						const minNumberLength = 8;

						for (let i = size - 1; i > -1; i--) {
							const number = parseInt(players.pop());

							if (isFinite(number) && ("" + number).length >= minNumberLength && !valid.includes(number)) {
								valid.push(number);
							}

							playersTV.setProgress('checkCurrentPlayers', size - i + '/' + size);
						}

						playersTV.setProgress('checkCurrentPlayers', 'Done');

						is[THIS_USERSCRIPT.NAME].players = valid;

						res();
					}

					playerTaskList.run();
				});
			}

			async function checkLastBlockListSync() {
				const lastBlocklistSync = is[THIS_USERSCRIPT.NAME].lastBlocklistSync;

				function setDefault() {
					is[THIS_USERSCRIPT.NAME].lastBlocklistSync = "";
				}
				
				if (typeof lastBlocklistSync != "string") {
					setDefault();
				}
				else if (lastBlocklistSync) {
					// see if it really is an ISO date
					const date = new Date(lastBlocklistSync);

					if (date == "Invalid Date") {
						setDefault();
					}
					else if (date.toISOString() != lastBlocklistSync) {
						setDefault();
					}
				}
			}

			async function checkThreadsToHide() {
				// await threadsTaskList.run(); doesn't work because goes past once all started to run
				await new Promise((res, rej) => {
					const threadsTasks = [checkMapOfTheWeekLegacy, checkHiddenThreadsLegacy, tidyUpThreadsStorage];
					const threadsTaskList = new GLOBALS.TaskList(threadsTasks, checkCurrentThreads);
					const threadsTV = new GLOBALS.TaskVisual(storageTV.find('#checkThreadsToHide'));

					threadsTV.setup(threadsTasks.concat(checkCurrentThreads));// doesn't change tasks (intentional)

					threadsTaskList.setOnTaskStart(taskName => {
						threadsTV.setProgress(taskName, 'Started');
					});
					threadsTaskList.setOnTaskCompletion(taskName => {
						threadsTV.setProgress(taskName, 'Done');
					});

					async function checkMapOfTheWeekLegacy() {
						if (localStorage.MOTW) {
							if (parseInt(localStorage.MOTW) == 1) {
								return "Map of the week discussion: Week ";
							}
						}

						return "";
					}

					async function checkHiddenThreadsLegacy() {
						if (!localStorage._threads) {
							return "";
						}

						const threadSubjects = localStorage._threads.split(/\,+/);
						const size = threadSubjects.length;
						threadsTV.setProgress('checkHiddenThreadsLegacy', '0/' + size);

						let initial = "";

						for (let i = size - 1; i > -1; i--) {
							const subject = threadSubjects.pop();

							initial += subject;

							if (i > 0 && subject) {// would be a blank subject
								initial += "\n";
							}

							threadsTV.setProgress('checkHiddenThreadsLegacy', size - i + '/' + size);
						}

						return initial;
					}

					async function tidyUpThreadsStorage() {
						const subKeys = ["str", "inReMode", "re", "flags"];
						let needsInitial = {
							str: true,
							inReMode: true,
							re: true,
							flags: true
						};

						if (typeof is[THIS_USERSCRIPT.NAME].hideThreads != "object" || Array.isArray(is[THIS_USERSCRIPT.NAME].hideThreads)) {
							is[THIS_USERSCRIPT.NAME].hideThreads = {};
						}

						for (let subKey in is[THIS_USERSCRIPT.NAME].hideThreads) {
							if (!subKeys.includes(subKey)) {
								delete is[THIS_USERSCRIPT.NAME].hideThreads[subKey];
							}
						}

						for (let subKey in needsInitial) {
							if (subKey == "inReMode") {
								needsInitial[subKey] = typeof is[THIS_USERSCRIPT.NAME].hideThreads[subKey] != "boolean";
							}
							else {
								needsInitial[subKey] = typeof is[THIS_USERSCRIPT.NAME].hideThreads[subKey] != "string";
							}
						}

						return needsInitial;
					}

					async function checkCurrentThreads(returnValues) {
						threadsTV.setProgress('checkCurrentThreads', 'Started');

						let initialStr = returnValues.checkMapOfTheWeekLegacy;

						if (initialStr) {
							initialStr += "\n";
						}

						initialStr += returnValues.checkHiddenThreadsLegacy;

						const needsInitial = returnValues.tidyUpThreadsStorage;

						if (needsInitial.str) {
							is[THIS_USERSCRIPT.NAME].hideThreads.str = initialStr;
						}
						if (needsInitial.inReMode) {
							is[THIS_USERSCRIPT.NAME].hideThreads.inReMode = false;
						}
						if (needsInitial.re) {
							is[THIS_USERSCRIPT.NAME].hideThreads.re = "";
						}
						if (needsInitial.flags) {
							is[THIS_USERSCRIPT.NAME].hideThreads.flags = "";
						}

						threadsTV.setProgress('checkCurrentThreads', 'Done');
						res();
					}

					threadsTaskList.run();
				});
			}

			async function checkHidingOT() {
				if (typeof is[THIS_USERSCRIPT.NAME].hideOT == "boolean") {
					return;
				}

				is[THIS_USERSCRIPT.NAME].hideOT = parseInt(localStorage.DanHTRBP_hidingOT) == 1;
			}

			async function checkHideBlankName() {
				if (typeof is[THIS_USERSCRIPT.NAME].hideBlankName == "boolean") {
					return;
				}

				is[THIS_USERSCRIPT.NAME].hideBlankName = parseInt(localStorage.DanHTRBP_hide_blank_name) == 1;
			}

			async function checkBlocklistedClans() {
				const key = "clans";
				const clanKeys = ["lastSync", "members", "keepList"];

				function validateArrayOfPlayerNumbers(clanId, clanKey) {
					const clan = is[THIS_USERSCRIPT.NAME][key][clanId];

					if (Array.isArray(clan[clanKey])) {
						for (let i = clan[clanKey].length - 1; i > -1; i--) {
							const member = parseInt(clan[clanKey][i]);

							if (!isFinite(member) || member < 10000000) {
								is[THIS_USERSCRIPT.NAME][key][clanId][clanKey].splice(i, 1);// invalid member
							}
						}
					}
					else {
						is[THIS_USERSCRIPT.NAME][key][clanId][clanKey] = [];
					}
				}

				if (typeof is[THIS_USERSCRIPT.NAME][key] == 'object' && !Array.isArray(is[THIS_USERSCRIPT.NAME][key])) {
					for (let clanId in is[THIS_USERSCRIPT.NAME][key]) {
						clanId = parseInt(clanId);

						if (!(isFinite(clanId) && clanId > 0)) {
							delete is[THIS_USERSCRIPT.NAME][key][clanId];// invalid clan id, delete the clan
						}

						const clan = is[THIS_USERSCRIPT.NAME][key][clanId];

						if (typeof clan != "object" || Array.isArray(clan)) {
							delete is[THIS_USERSCRIPT.NAME][key][clanId];// not a clan
						}
						else {
							for (let clanKey in clan) {
								if (!clanKeys.includes(clanKey)) {
									delete is[THIS_USERSCRIPT.NAME][key][clanId][clanKey];// invalid key
								}
							}

							validateArrayOfPlayerNumbers(clanId, "keepList");
							validateArrayOfPlayerNumbers(clanId, "members");

							if (typeof clan.lastSync != "string" && clan.lastSync !== undefined) {
								delete is[THIS_USERSCRIPT.NAME][key][clanId].lastSync;// can still have a clan, but not known when clan was last got
							}
						}
					}
				}
				else {
					is[THIS_USERSCRIPT.NAME][key] = {};
				}
			}

			async function finalise() {
				storageTV.setProgress('finalise', 'Started');

				discontinued.concat(legacy).forEach((item) => {
					localStorage.removeItem(item);
				});

				for (let key in is[THIS_USERSCRIPT.NAME]) {
					if (!validStorageLocationsForThisUserscript.includes(key)) {
						delete is[THIS_USERSCRIPT.NAME][key];
					}
				}

				if (needsToUpdateStorage) {
					const updateMsg = storageProgressVisual.querySelector('#msg') || storageProgressVisual;

					updateMsg.innerText = "Storage for " + GLOBALS.cammelCaseToTitle(THIS_USERSCRIPT.NAME) + " updated.";
				}

				storageTV.setProgress('finalise', 'Done');

				resolve(is);
			}

			taskList.run();
		});
	}

	async function importLegacy(toImport) {
		// set the local storage options then go by normal process

		const parsed = JSON.parse(toImport);

		localStorage.Players = parsed[0];
		localStorage.threads = parsed[1];
		localStorage._threads = parsed[2];
		localStorage.MOTW = parsed[3];
		localStorage.DanHTRBP_hidingUI = parsed[4];
		localStorage.DanHTRBP_hidingOT = parsed[5];
		localStorage.DanHTRBP_hide_blank_name = parsed[6];
		localStorage.DanHTRBP_hide_threads = parsed[7];
		localStorage.DanHTRBP_hide_replies = parsed[8];

		return (await GLOBALS.storage.validateCorrectingErrors(THIS_USERSCRIPT.NAME))[THIS_USERSCRIPT.NAME];
	}

	// main userscript goes here
	// warlight.com/Profile?p=5614353942 is meant to fail
	const profileLinkEgs = `The following link formats are supported
https://www.warzone.com/Profile?p=5614353942
https://warzone.com/Profile?p=5614353942
http://www.warzone.com/Profile?p=5614353942
http://warzone.com/Profile?p=5614353942
www.warzone.com/Profile?p=5614353942
warzone.com/Profile?p=5614353942
https://www.warlight.net/Profile?p=5614353942
https://warlight.net/Profile?p=5614353942
http://www.warlight.net/Profile?p=5614353942
http://warlight.net/Profile?p=5614353942
www.warlight.net/Profile?p=5614353942
warlight.net/Profile?p=5614353942`;

	function getBlocklistedMembers(clan) {
		return clan.members.filter((member) => {
			return !clan.keepList.includes(member);
		});
	}

	function getPlayerNumbers(profileLinks) {
		if (!Array.isArray(profileLinks)) {
			console.table('profileLinks', profileLinks);
			throw new Error("profileLinks must be an array");
		}

		const numbers = [];

		for (let i = profileLinks.length - 1; i > -1; i--) {
			const link = profileLinks.pop();
			let number;

			if (typeof link == 'number' && isFinite(link) && link >= 10000000) {
				number = link;
			}
			else if (typeof link != "string") {
				console.table('link', link);
				throw new Error("profile link must be a string");
			}
			else {
				number = link.match(profileLinkRe);
				if (!number) {
					console.log("invalid profile link found, skipping");
				}
				else {
					number = parseInt(number[1]);
				}
			}

			if (!numbers.includes(number)) {
				numbers.push(number);
			}
		}

		return numbers;
	}

	async function getClanIndex(id) {
		id = parseInt(id);

		if (!isFinite(id) || id < 1) {
			throw new Error("id must be a positive number");
		}

		const clans = await getStorageItem("clans");

		if (clans[id]) {
			return id;
		}

		return -1;
	}

	const config = {
		version: THIS_USERSCRIPT.VERSION,
		blocklist: {
			view: async () => {
				return await getStorageItem("players");
			},
			syncWithWZ: async function(keepList, keepCurrentBlocklist) {
				if (!Array.isArray(keepList)) {
					throw new Error("keepList must be an Array of Strings where the String is a profile link");
				}

				keepCurrentBlocklist = !!keepCurrentBlocklist;

				const blocklist = await GLOBALS.extract.ownBlocklist();

				await setStorageItem("lastBlocklistSync", new Date().toISOString());

				if (keepCurrentBlocklist) {
					keepList = await this.view().then((players) => {return players.concat(keepList);});
				}

				await this.overwrite(keepList.concat(blocklist));

				return "Done";
			},
			lastBlocklistSync: async () => {
				return await getStorageItem("lastBlocklistSync");
			},
			add: async function(playerLinks) {
				const existing = await this.view();
				const players = getPlayerNumbers(playerLinks);

				for (let i = players.length - 1; i > -1; i--) {
					const number = players.pop();

					if (!existing.includes(number)) {
						existing.push(number);
					}
				}

				await setStorageItem("players", existing);
			},
			remove: async function(playerLinks) {
				const existing = await this.view();
				const players = getPlayerNumbers(playerLinks);

				for (let i = players.length - 1; i > -1; i--) {
					const number = players.pop();
					const index = existing.indexOf(number);

					if (index > -1) {
						existing.splice(index, 1);
					}
				}

				await setStorageItem("players", existing);
			},
			overwrite: async (playerLinks) => {
				await setStorageItem("players", getPlayerNumbers(playerLinks));
			},
			clans: {
				add: async (id) => {
					const clans = await getStorageItem("clans");
					const players = GLOBALS.storage.SHARED.getItem('players');
					const clanInfo = await new Promise((resolve, reject) => {
						const clanWindow = open("https://www.warzone.com/Clans/?ID=" + id);
						const clan = GLOBALS.storage.SHARED.getClan(id);

						clan.members = {};

						clanWindow.onload = async () => {
							await GLOBALS.extract.clanMembers(clanWindow, (member, totalClanMembers) => {
								const player = {
									name: member.name,
									title: member.title,
									clan: id
								};

								players[member.number] = player;
								clan.members[member.number] = 1;
							})
							.catch(err => {
								clanWindow.close();
								reject(err);
							});

							clanWindow.close();
							resolve(clan);
						};
					});
					clanInfo.lastUpdate = new Date().toUTCString();

					const clan = {
						keepList: [],
					};

					clans[id] = clan;

					await setStorageItem("clans", clans);
					await GLOBALS.storage.SHARED.setClan(id, clanInfo);
					await GLOBALS.storage.SHARED.setItem('players', players);

					return "Done";
				},
				remove: async (id, deleteClanBtn) => {
					const clans = await getStorageItem("clans");
					delete clans[id];
					await setStorageItem("clans", clans);

					await GLOBALS.storage.SHARED.deleteClan(id, deleteClanBtn);
				},
				list: async function() {
					const list = await getStorageItem("clans");

					function addFuncs(clanId) {
						list[clanId].sync = async (keepList, preserveCurrentKeepList) => {
							if (!Array.isArray(keepList)) {
								keepList = [];
							}

							if (preserveCurrentKeepList) {
								keepList = keepList.concat(list[clanId].keepList);
							}

							await this.add(clanId);
							const clans = await getStorageItem("clans");
							clans[clanId].keepList = keepList;
							await setStorageItem("clans", clans);
						};
						list[clanId].overwriteKeepList = async(keepList) => {
							const clans = await getStorageItem("clans");
							clans[clanId].keepList = getPlayerNumbers(keepList);
							await setStorageItem("clans", clans);
						};
					}

					for (let clanId in list) {
						addFuncs(clanId);
					}

					return list;
				}
			}
		},
		threads: {
			hideUsingStr: async (str) => {
				if (typeof str != "string") {
					throw new Error("str must be a string");
				}

				const hideThreads = await getStorageItem("hideThreads");

				hideThreads.str = str;
				await setStorageItem("hideThreads", hideThreads);
			},
			hideUsingRe: async (re) => {
				if (!(re instanceof RegExp)) {
					throw new Error("re must be a RegExp");
				}

				const hideThreads = await getStorageItem("hideThreads");

				hideThreads.re = re.source;
				hideThreads.flags = re.flags;
				await setStorageItem("hideThreads", hideThreads);
			},
			useReMode: async (useRe) => {
				const hideThreads = await getStorageItem("hideThreads");

				hideThreads.inReMode = !!useRe;
				await setStorageItem("hideThreads", hideThreads);
			}
		},
		settings: {
			import: async (imported) => {
				await GLOBALS.storage.import(imported);
			},
			export: async () => {
				await GLOBALS.storage.export();
			},
			reset: async () => {
				await GLOBALS.storage[THIS_USERSCRIPT.NAME].reset();
			}
		}
	};

	function canHideThreads() {
		const threadPages = [/^https:\/\/www\.warzone\.com\/Clans\/Forum(?:\?offset=-?\d+)?/i, /^https:\/\/www\.warzone\.com\/Discussion\/MyMail(?:\?(?:(?:redir=1(?:&offset=-?\d+)?)|(?:offset=-?\d+)))?/i, /^https:\/\/www\.warzone\.com\/forum\/(?:(Forum)|(?:f\d+(?:(?:-|\w+)+)?))/i];

		/*
		https://www.warzone.com/Discussion/MyMail?redir=1
		https://www.warzone.com/Discussion/MyMail
		https://www.warzone.com/Discussion/MyMail?redir=1&offset=100
		https://www.warzone.com/Discussion/MyMail?offset=100
		https://www.warzone.com/Discussion/MyMail
		https://www.warzone.com/Discussion/MyMail?&offset=100 this is meant to not match; however wz still lists threads though
		https://www.warzone.com/Discussion/MyMail&offset=100 this is meant to not match; wz gives error on this page

		https://www.warzone.com/Forum/f1-General
		https://www.warzone.com/Forum/f4-Map-Development
		https://www.warzone.com/Forum/f5-Ladder
		https://www.warzone.com/Forum/f6-Programming
		https://www.warzone.com/Forum/f7-Help
		https://www.warzone.com/Forum/f8-topic
		https://www.warzone.com/Forum/f9-Clans
		// https://www.warzone.com/Forum/f10-Strategy deleted, redirects to error
		https://www.warzone.com/Forum/f11-Warzone-Idle
		https://www.warzone.com/Forum/Forum
		https://www.warzone.com/Forum/f1? is meant to match; wz makes sense of it
		https://www.warzone.com/Forum/Forum?& is meant to match; wz makes sense of it
		*/

		for (let i = threadPages.length - 1; i > -1; i--) {
			const hasMatch = window.location.href.match(threadPages.pop());

			if (hasMatch) {
				return {onAllForm: !!hasMatch[1]};
			}
		}
	}
	function canHideReplies() {
		const replyPages = [/^https:\/\/www\.warzone\.com\/Forum\/\d+/i, /^https:\/\/www\.warzone\.com\/Discussion\/\?ID=\d+/i];

		for (let i = replyPages.length - 1; i > -1; i--) {
			const hasMatch = window.location.href.match(replyPages.pop());

			if (hasMatch) {
				return !!hasMatch;
			}
		}
	}

	function getThreads() {
		const threads = [];
		// somehow splitting it link this appears to be fast
		const forumThread = /^https:\/\/www\.warzone\.com\/Forum\/\d+/i;
		const discussionThread = /^https:\/\/www\.warzone\.com\/Discussion\/\?ID=\d+/i;
		const offset = /\?offset=\d+$/i;

		function addThread(link) {
			const threadElement = link.parentNode.parentNode;

			threads.push({
				dom: threadElement,
				link: link,
				subject: link.innerText.trim(),
				hide: () => {
					threadElement.dataset.blocked = 1;
				},
				show: () => {
					threadElement.removeAttribute("data-blocked");
				},
				isHidden: () => {
					return !!threadElement.dataset.blocked;
				}
			});
		}

		for (let link of document.links) {
			if (!link.href.match(offset)) {
				if (link.href.match(forumThread)) {
					addThread(link);
				}
				else if (link.href.match(discussionThread)) {
					addThread(link);
				}
			}
		}

		return threads;
	}

	async function getCreator(threadId, threadText) {
		const thread = await GLOBALS.storage.SHARED.getThread(threadId);
		const needsUpdate = (new Date() - new Date(thread.lastUpdate) > (1000 * 60 * 60 * 24 * 7)) || (!thread.creatorId);
		// if it's been longer than a week since last update or not set, update
		
		if (needsUpdate) {
			const catData = threadText.match(/>&lt;&lt; Back to (?:(?:([a-z\s-]+(?=Forum))Forum)|(My Mail))/i);

			if (catData[1] && !catData[1].match(/^clan $/)) {
				thread.category = 'Official: ' + catData[1];
			}
			else {
				let cat = threadText.match(/<div class="DiscussionPostDiv" style="display:block; width: 100%;\s*overflow-x: auto" id="PostForDisplay_0">(?:(?:\s)|(?:<pre class="prettyprint">~((?:\w|\s)+?)<\/pre>)|.)+?<\/div>/) || null;
				// matches the first category in the first post

				if (cat) {
					cat = cat[0].match(/(?:<pre class="prettyprint">~((?:\w|\s)+?)<\/pre>)/);

					if (cat) {
						cat = cat[1];
					}
				}

				if (cat) {
					thread.category = cat;
				}
				else {
					delete thread.category;
				}
			}

			await GLOBALS.readFullThreadPage(threadText, true, async (poster, clanData, postNum) => {
				// updates the shared player
				const playerDetails = await GLOBALS.storage.SHARED.getPlayer(poster.number);

				playerDetails.name = poster.name;
				playerDetails.clan = poster.clan;

				await GLOBALS.storage.SHARED.setPlayer(poster.number, playerDetails);

				if (postNum == 0) {
					thread.creatorId = poster.number;
					thread.lastUpdate = new Date().toUTCString();
				}
			});

			await GLOBALS.storage.SHARED.setThread(threadId, thread);
		}

		const creator = await GLOBALS.storage.SHARED.getPlayer(thread.creatorId);

		return {
			number: thread.creatorId,
			name: creator.name,
			clan: creator.clan == 0 ? NaN : creator.clan
		};
	}

	function linesToRegex(string) {
		const lines = GLOBALS.escapeRegExp(string).split(/\n+/);
		let regex = "";

		for (let i = lines.length - 1; i > -1; i--) {
			const line = lines.pop();

			// have to remove blank line so that every thread isn't hidden
			if (line.length) {
				regex += `(?:${line})`;

				if (i - 1 > -1) {
					regex += "|";
				}
			}
		}

		return regex ? new RegExp(regex) : null;
	}

	function isBlank(str) {
		if (typeof str != "string") {
			return;
		}

		// \s, last version and https://stackoverflow.com/questions/18169006/all-the-whitespace-characters-is-it-language-independent
		const blankChars = /[\s\u00AD\u2800]+/g;
		let totalMatchLength = 0;

		const matches = str.match(blankChars);

		if (matches) {
			matches.forEach((match) => {
				totalMatchLength += match.length;
			});
		}

		return str.length == totalMatchLength;
	}

	const Threads = {
		busyHiding: false,
		hidden: false,
		hideShow: function() {
			if (this.hidden) {
				this.show();
			}
			else {
				this.hide();
			}
		},
		blockedThreadsArea: {
			id: "blockedThreadsArea",
			get: function() {
				return document.getElementById(this.id);
			},
			_parent: null,
			_onAllForm: null,
			create: function(parent, onAllForm) {
				let area = this.get();

				if (area) {
					return area;
				}

				if (parent instanceof HTMLElement) {
					this._parent = parent;
				}
				if (typeof onAllForm == 'boolean') {
					this._onAllForm = onAllForm;
				}

				area = document.createElement("tr");
				area.id = this.id;
				area.style.cursor = 'pointer';

				let numCols = 4;
				let subjectCol = 1;

				if (this._onAllForm) {
					numCols++;
					subjectCol++;
				}

				for (let i = 0; i < numCols; i++) {
					const child = document.createElement("td");

					if (i == subjectCol) {
						child.id = Threads.numBlockedThreads.id;
					}

					area.appendChild(child);
				}

				this._parent.appendChild(area);

				document.getElementById(Threads.numBlockedThreads.id).parentNode.onclick = hideShowThreads;
			}
		},
		hide: async function() {
			let providedRegExIsBad = false;
			const canHide = canHideThreads();

			if (!canHide || this.busyHiding) {
				return;
			}

			this.busyHiding = true;

			const canHideOT = canHide.onAllForm && await getStorageItem("hideOT");
			const canHideBlankName = await getStorageItem("hideBlankName");
			const threads = getThreads();
			const threadsTable = threads[0].dom.parentNode;
			const blocklist = await config.blocklist.view();
			const clans = await config.blocklist.clans.list();
			const hideThreads = await getStorageItem("hideThreads");
			let hideBySubjectRe;

			let numBlockedThreads = 0;
			function countThread(thread) {
				numBlockedThreads++;

				const toNotMove = ["hidden", "blocked"];
				let canMove = true;

				for (let data of toNotMove) {
					if (data in thread.dom.dataset) {
						canMove = false;
						break;
					}
				}

				thread.hide();

				if (canMove) {
					threadsTable.insertAdjacentElement("beforeend", thread.dom);
				}
			}

			async function hideThread(threads, i) {
				const thread = threads[i];

				// this is window for some reason
				Threads.numBlockedThreads.setText('Hiding threads… processed ' + i + ' of ' + threads.length);

				const threadDetails = await fetchThread(thread.link.href);
				const creator = await getCreator(threadDetails.id, threadDetails.text);

				if (creator == null) {
					// thread could have been deleted
					return;
				}
				
				readMailParticipants(threadDetails.id, threadDetails.text);

				if (canHideOT && thread.dom.firstElementChild.innerText == "Off-topic") {
					countThread(thread);
				}
				else if (blocklist.includes(creator.number)) {
					countThread(thread);
				}
				else if (canHideBlankName && isBlank(creator.name)) {
					countThread(thread);
				}
				else if (hideBySubjectRe && thread.subject.match(hideBySubjectRe)) {
					countThread(thread);
				}
				else {
					if (isNaN(creator.clan)) {
						// might not have a clan
						return;
					}

					const clanIndex = await getClanIndex(creator.clan);

					if (clanIndex == -1) {
						// clan might not be block listed
						return;
					}
	
					if (getBlocklistedMembers(clans[clanIndex]).includes(creator)) {
						countThread(thread);
					}
				}
			}

			this.blockedThreadsArea.create(threads[0].dom.parentNode.parentNode, canHide.onAllForm);

			try {
				if (hideThreads.inReMode) {
					try {
						hideBySubjectRe = new RegExp(hideThreads.re, hideThreads.flags);
					}
					catch(err) {
						providedRegExIsBad = true;

						throw new Error("The regex you've provided is broken. Fix it.\n" + err);
					}
				}
				else {
					hideBySubjectRe = linesToRegex(hideThreads.str);
				}

				for (let i  = 0; i < threads.length; i++) {
					await hideThread(threads, i);
				}

				this.numBlockedThreads.setText('Show ' + numBlockedThreads + ' blocked thread' + (numBlockedThreads === 1 ? '' : 's'));
				this.busyHiding = false;
				this.hidden = true;
			}
			catch(err) {
				if (providedRegExIsBad) {
					alert(err);
				}
				else {
					console.exception(err);
				}
			}
		},
		show: function() {
			const canHide = canHideThreads();

			if (!canHide || this.busyHiding) {
				return;
			}

			const threads = document.body.querySelectorAll("[data-blocked]");

			for (let i = 0; i < threads.length; i++) {
				threads[i].removeAttribute("data-blocked");
			}

			this.numBlockedThreads.setText(this.numBlockedThreads.text().replace("Show", "Hide"));
			this.hidden = false;
		},
		numBlockedThreads: {
			id: "numBlockedThreads",
			get: function() {
				return document.getElementById(this.id);
			},
			setText: function(text) {
				const is = this.get();

				if (is) {
					is.innerText = text;
				}
			},
			text: function() {
				const is = this.get();

				return is ? is.innerText : '';
			}
		}
	};

	function hideShowThreads() {
		// can't access Threads before it's fully defined, this parent object
		Threads.hideShow();
	}

	function updateMenu() {
		// this is redefined
		// doesn't know which part to update (too messy)
		// so just remake it, better than displaying out of date settings
	}

	function doHiding() {
		// refresh threads
		Threads.show();
		Threads.hide();

		hideReplies();
	}

	async function setStorageItem(key, value, dontDoHiding) {
		await GLOBALS.storage[THIS_USERSCRIPT.NAME].setItem(key, value);

		if (dontDoHiding) {
			return;
		}

		doHiding();
	}
	async function getStorageItem(key) {
		return await GLOBALS.storage[THIS_USERSCRIPT.NAME].getItem(key);
	}

	async function hideReplies() {
		if (!canHideReplies()) {
			return;
		}

		function createUnBlocklistBtns(posterArea, posterNumber, posterIsBLed, clanIndex) {
			const link = "https://www.warzone.com/Profile?p=" + posterNumber;
			const container = document.createElement("div");
			const blId = "bl_" + posterNumber;
			const unblId = "unbl_" + posterNumber;
			const cn = "blUnblContainer";

			container.className = cn;
			container.innerHTML = `<input id="${blId}" type="button" value="+ Block list">&nbsp;<input id="${unblId}" type="button" value="- Block list">`;

			const existingContainers = posterArea.getElementsByClassName(cn);
			if (existingContainers.length) {
				// replace the old one
				existingContainers[0].remove();
			}

			posterArea.appendChild(container);

			const blBtn = container.querySelector('#' + blId);
			const unblBtn = container.querySelector('#' + unblId);

			if (posterIsBLed) {
				blBtn.style.display = "none";
			}
			else {
				unblBtn.style.display = "none";
			}

			blBtn.onclick = function() {
				config.blocklist.add([link]).then(() => {
					updateMenu();
				});
			};

			unblBtn.onclick = async function() {
				await config.blocklist.remove([link]);

				if (posterIsBLed && clanIndex > -1) {
					// could be blocked due to clan, add to whitelist
					const clan = (await config.blocklist.clans.list())[clanIndex];
					const newKeepList = clan.keepList.concat([link]);

					await clan.overwriteKeepList(newKeepList);
				}

				updateMenu();
			};
		}

		function createViewHiddenReplyBtn(postTbl, isHidden, isDownvoted, posterName) {
			const unhideBtn = document.createElement("font");			
			let text = "- ";

			if (isHidden) {
				// easier to remove then create own
				if (isDownvoted) {
					text += "downvoted";
				}
				else {
					text += "hidden";
				}

				text += " and ";

				postTbl.previousElementSibling.remove();
			}

			text += "blocked post by " + posterName;

			unhideBtn.innerText = text;
			unhideBtn.className = 'unhideBtn';
			unhideBtn.style.cursor = 'pointer';
			unhideBtn.onclick = function() {
				unhideBtn.remove();
				postTbl.removeAttribute("data-blocked");
			};

			postTbl.insertAdjacentElement("beforebegin", unhideBtn);
		}

		const replies = document.body.querySelectorAll("table[id ^= PostTbl]");
		const bl = await config.blocklist.view();

		for (let postTbl of replies) {
			const isHidden = postTbl.style.display == "none";
			const isDownvoted = isHidden && postTbl.previousElementSibling.innerText.match(/^- downvoted/);
			const posterArea = postTbl.firstElementChild.lastElementChild.firstElementChild;
			const poster = posterArea.querySelector("a[href ^= '/Profile']");
			const posterName = poster.innerText;
			const posterNumber = parseInt(poster.href.match(/\d+/)[0]);
			const posterClan = posterArea.querySelector("a[href ^= '/Clans/']");
			let posterIsBLed = bl.includes(posterNumber);
			let clanIndex = -1;

			if (!posterIsBLed && await getStorageItem("hideBlankName")) {
				posterIsBLed = isBlank(posterName);
			}

			if (!posterIsBLed && posterClan) {
				const clanId = parseInt(posterClan.href.match(/\d+/)[0]);
				const clans = await config.blocklist.clans.list();
				clanIndex = await getClanIndex(clanId);

				if (clanIndex > -1) {
					posterIsBLed = getBlocklistedMembers(clans[clanIndex]).includes(posterNumber);
				}
			}

			createUnBlocklistBtns(posterArea, posterNumber, posterIsBLed, clanIndex);

			if (postTbl.previousElementSibling.className.match('unhideBtn')) {
				// get rid of old one then show the reply
				postTbl.previousElementSibling.click();
			}

			if (posterIsBLed) {
				postTbl.dataset.blocked = 1;
				createViewHiddenReplyBtn(postTbl, isHidden, isDownvoted, posterName);
			}
		}
	}

	async function readMailParticipants(threadId, threadStr) {
		if (!location.href.match(/^https:\/\/www\.warzone\.com\/Discussion\/MyMail/)) {
			return;
		}

		const participantRe = "(?:\\s*<a href=\"\\/Profile\\?p=(\\d+)\"> ([^<]+)<\\/a>(?:\\s+,)?)";
		const fullRe = "<h1>Mail Thread<\\/h1>[^:]+:" + participantRe + '+';

		let pmParticipants = await getStorageItem('pmParticipants') || {};

		pmParticipants[threadId] = [];// for if the feature to add or remove players to/from a pm is ever created 

		const participants = threadStr.match(fullRe)[0].split(/<a/);
		for (let i = 1; i < participants.length; i++) {
			const p = ('<a' + participants[i]).match(participantRe);
			const players = await GLOBALS.storage.SHARED.getItem('players');
			const pNo = parseInt(p[1]);

			if (!players[pNo]) {
				players[pNo] = {};
			}

			players[pNo].name = p[2];
			pmParticipants[threadId].push(pNo);

			await GLOBALS.storage.SHARED.setItem('players', players);
		}

		await setStorageItem('pmParticipants', pmParticipants, true);
		await displayMailParticipants(threadId);
	}

	async function displayMailParticipants(threadId) {
		if (!location.href.match(/^https:\/\/www\.warzone\.com\/Discussion\/MyMail/)) {
			return;
		}

		const threadLink = document.querySelector('a[href$="ID=' + threadId + '"');
		const playerNames = threadLink.nextElementSibling.nextElementSibling;

		playerNames.innerHTML = '';

		const pmParticipants = (await getStorageItem('pmParticipants'))[threadId];
		for (let i = 0; i < pmParticipants.length; i++) {
			const pNo = pmParticipants[i];
			const name = (await GLOBALS.storage.SHARED.getItem('players'))[pNo].name;

			playerNames.innerHTML += name;

			if (i + 1 < pmParticipants.length) {
				playerNames.innerHTML += ', ';
			}
		}
	}

	async function fetchThread(threadLink) {
		return {
			id: parseInt(threadLink.match(/\d+/)[0]),
			text: await fetchText(threadLink)
		};
	}

	if (self == top && !window.opener) {
		getCommonCode().then(() => {
			GLOBALS.deepFreeze(config);
			window.configDan = config;

			doHiding();
		});
	}
})();