diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..33d2cfa --- /dev/null +++ b/.prettierrc @@ -0,0 +1,4 @@ +{ + "arrowParens": "avoid", + "semi": false +} diff --git a/README.md b/README.md index f38ec51..8080d86 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,11 @@ # Wikidot applications deletion userscript -[Tampermonkey](https://www.tampermonkey.net/) userscript for -[Wikidot](https://www.wikidot.com/) users. +[Tampermonkey](https://www.tampermonkey.net/) userscript for [Wikidot](https://www.wikidot.com/) users. Adds two buttons to the messages inbox: -* **Delete recent applications:** Deletes applications on the first page of - the user's inbox, then the second, and so on until a page is found that - already has no applications. -* **Delete all applications:** Deletes all applications in the user's - inbox. +* **Delete recent applications:** Deletes applications on the first page of the user's inbox, then the second, and so on until a page is found that already has no applications. +* **Delete all applications:** Deletes all applications in the user's inbox.
@@ -17,28 +13,27 @@ Adds two buttons to the messages inbox:
Before deletion is committed, a confirmation dialogue will be raised.
-Especially useful for Wikidot administrators of popular sites, whose
-inboxes will quickly become full of applications, drowning out actual
-messages from other users. Use at own risk.
+Especially useful for Wikidot administrators of popular sites, whose inboxes will quickly become full of applications, drowning out actual messages from other users. Use at own risk.
Installation instructions: https://scpwiki.com/usertools#userscripts
## Installation via Tampermonkey
+This method permanently adds the two buttons to your Wikidot inbox. They will be there for as long as you have both Tampermonkey and this userscript installed.
+
1. Install [Tampermonkey](https://www.tampermonkey.net/).
-2. Visit the [userscript
- directly](https://github.com/croque-scp/delete-applications/raw/main/delete-applications.user.js).
-3. Tampermonkey will prompt you to install the userscript. Click 'install'
- to do so, being sure to review the code first.
+2. Visit the [userscript directly](https://github.com/croque-scp/delete-applications/raw/main/delete-applications.user.js).
+3. Tampermonkey will prompt you to install the userscript. Click 'install' to do so, being sure to review the code first.
+4. Visit your [Wikidot inbox](https://www.wikidot.com/account/messages). The two buttons will be there.
+
+Uninstallation: Go to your Tampermonkey dashboard, which can be found in your browser extensions page. Click the bin icon next to the 'Wikidot applications deleter' script.
## Usage without Tampermonkey
-1. Visit the [userscript
- directly](https://github.com/croque-scp/delete-applications/raw/main/delete-applications.user.js)
- and copy the whole thing.
-2. Visit your [Wikidot inbox](https://www.wikidot.com/account/messages) and
- open the JavaScript console.
-3. Paste the userscript into the console and press enter.
-4. Enter one of the following, and then press enter:
- * `deleteApplications()` to delete recent applications
- * `deleteApplications(true)` to delete all applications
+This method adds the two buttons to your Wikidot inbox once only. They will no longer be there as soon as you leave the page.
+
+1. Visit the [userscript directly](https://github.com/croque-scp/delete-applications/raw/main/delete-applications.user.js) and copy the whole thing.
+2. Visit your [Wikidot inbox](https://www.wikidot.com/account/messages) and open the JavaScript console.
+3. Paste the userscript into the console and press enter. The two buttons will appear.
+
+This is a one-off process that must be repeated every time you want to delete applications. Use this method if you don't want to (or can't) install this tool as a Tampermonkey userscript.
\ No newline at end of file
diff --git a/delete-applications.user.js b/delete-applications.user.js
index 280c0a6..7959d55 100644
--- a/delete-applications.user.js
+++ b/delete-applications.user.js
@@ -4,6 +4,31 @@ Wikidot applications deleter userscript
For installation instructions, see https://scpwiki.com/usertools
*/
+/* CHANGELOG
+
+v1.3.0
+- Added changelog.
+- Removed extra commas from the confirmation popup when deleting applications from more than one site.
+- Deletes now execute in batches of 100 separated by a short delay to bypass Wikidot's single-request limit of 996.
+- Made buttons larger and added more support links.
+
+v1.2.0 (2023-07-07)
+- Added a list of sites to the deletion confirmation popup that tells you which Wikidot sites the applications come from, and how many there are per site.
+
+v1.1.0 (2022-04-11)
+- Added new feature 'delete recent applications' that deletes applications page-by-page until encountering a page with no applications.
+- Removed feature 'delete applications on current page'.
+- After scanning pages of messages, script now puts you back on the first page instead of leaving you wherever it stopped.
+- The delete buttons are now visible on all pages of the inbox instead of just the first.
+
+v1.0.1 (2022-03-06)
+- Hid buttons when reading a message.
+- Fixed deletion confirmation popup interfering with message composer UI.
+
+v1.0.0 (2022-03-01)
+- Created userscript.
+*/
+
// ==UserScript==
// @name Wikidot applications deleter
// @description Adds a button to delete applications from your Wikidot inbox.
@@ -17,45 +42,99 @@ For installation instructions, see https://scpwiki.com/usertools
/* global WIKIDOT, OZONE */
-let deleteButtonsContainer
+/* ===== Utilities ===== */
const deleterDebug = log => console.debug("Applications deleter:", log)
+const supportUser = showAvatar => `
+ ${
+ showAvatar
+ ? ``
+ : ""
+ }
+
+ ${
+ showAvatar
+ ? `
`
+ : ""
+ }Croquembouche
+
+ ${showAvatar ? `` : ""}
+`
+
+function getMessagesOnPage() {
+ return Array.from(document.querySelectorAll("tr.message")).map(
+ el => new Message(el)
+ )
+}
+
+function countSelected(messages) {
+ return messages.reduce((a, b) => a + b.isSelected, 0)
+}
+
+class Counter {
+ constructor(array) {
+ array.forEach(val => (this[val] = (this[val] || 0) + 1))
+ }
+}
+
/**
- * Collates details about a message based on its little preview.
+ * Waits for the given number of milliseconds.
+ * @param {Number} ms
*/
+async function wait(ms) {
+ return new Promise(resolve => setTimeout(resolve, ms))
+}
+
class Message {
+ /**
+ * Collates details about a message based on its little preview.
+ * @param {HTMLElement} messageElement - Inbox container.
+ */
constructor(messageElement) {
+ /** @type {HTMLInputElement} */
this.selector = messageElement.querySelector("input[type=checkbox]")
+ /** @type {String} */
this.id = this.selector.value
// Extract the sender and the subject
const from = messageElement.querySelector("td .from .printuser")
- this.fromWikidot = (
+ this.fromWikidot =
!from.classList.contains("avatarhover") && from.innerText === "Wikidot"
- )
this.subject = messageElement.querySelector(".subject").innerText
this.previewText = messageElement.querySelector(".preview").innerText
// Is this message an application?
- this.isApplication = (
- this.fromWikidot
- && this.subject === "You received a membership application"
- )
+ this.isApplication =
+ this.fromWikidot &&
+ this.subject === "You received a membership application"
if (this.isApplication) {
// Which wiki is the application for?
- const wikiMatch = this.previewText.match(/applied for membership on (.*), one of your sites/)
+ const wikiMatch = this.previewText.match(
+ /applied for membership on (.*), one of your sites/
+ )
if (wikiMatch) this.applicationWiki = wikiMatch[1]
else this.isApplication = false
}
}
- select() { this.selector.checked = true }
- deselect() { this.selector.checked = false }
- get isSelected() { return this.selector.checked }
+ select() {
+ this.selector.checked = true
+ }
+ deselect() {
+ this.selector.checked = false
+ }
+ get isSelected() {
+ return this.selector.checked
+ }
}
+/* ===== */
+
async function deleteApplications(deleteAll = false) {
const applications = []
const messageElement = document.getElementById("message-area")
@@ -63,7 +142,11 @@ async function deleteApplications(deleteAll = false) {
let goToNextPage = true
let thereAreMorePages = true
- firstPage(messageElement)
+ const scanningModal = new OZONE.dialogs.WaitBox()
+ scanningModal.content = "Scanning your inbox for applications..."
+ scanningModal.show()
+
+ await firstPage(messageElement)
do {
const messages = getMessagesOnPage()
@@ -79,86 +162,169 @@ async function deleteApplications(deleteAll = false) {
})
// Save all selected messages
- const selectedMessages = messages
- .filter(message => message.isSelected)
+ const selectedMessages = messages.filter(message => message.isSelected)
deleterDebug(`Found ${selectedMessages.length} applications`)
applications.push(selectedMessages)
- // If there were no selected messages, and we are only deleting recent
- // messages (i.e. deleteAll is false), don't go to the next page
+ // If there were no selected messages, and we are only deleting recent messages (i.e. deleteAll is false), don't go to the next page
if (selectedMessages.length === 0 && !deleteAll) goToNextPage = false
if (goToNextPage) thereAreMorePages = await nextPage(messageElement)
} while (goToNextPage && thereAreMorePages)
- // Delete all saved messages
- deleteMessages(applications.flat())
-
- firstPage(messageElement)
-}
+ // Reset UI back to the first page
+ await firstPage(messageElement)
-function getMessagesOnPage() {
- return Array.from(
- document.querySelectorAll("tr.message")
- ).map(el => new Message(el))
+ // Delete all saved messages
+ createDeleteConfirmationModal(applications.flat())
}
-function countSelected(messages) {
- return messages.reduce((a, b) => a + b.isSelected, 0)
-}
+/**
+ * @param {Message[]} messages
+ */
+function createDeleteConfirmationModal(messages) {
+ const messagesCount = messages.length
-function deleteMessages(messages) {
// Collate the wikis that the applications were for
const wikiCounter = new Counter(messages.map(m => m.applicationWiki))
// Produce a confirmation modal with the number of applications to delete
const confirmModal = new OZONE.dialogs.ConfirmationDialog()
+ const applicationSitesList = Object.entries(wikiCounter).map(
+ ([wiki, count]) => `
Delete ${messages.length} applications?
-Delete ${messagesCount} applications?
+Please report any issues during the deletion process to ${supportUser( + true + )}.
+Deleting ${messagesCount} applications...
+ + + ` + progressModal.timeout = null + progressModal.show() + + const success = await deleteMessagesBatches( + messages, + async (batchIndex, batchCount, batchSize) => { + if (batchCount === 1) return + document.getElementById("delete-progress-text").textContent = ` + Batch ${batchIndex + 1} of ${batchCount} (${batchSize} applications) + ` + document.getElementById("delete-progress").max = batchCount + document.getElementById("delete-progress").value = batchIndex + 1 + await wait(1500) + } + ) + + WIKIDOT.modules.DashboardMessagesModule.app.refresh() + + if (success) { const successModal = new OZONE.dialogs.SuccessBox() - successModal.content = "Deleted applications." + successModal.content = ` +Deleted ${messagesCount} applications.
+ ` successModal.show() - WIKIDOT.modules.DashboardMessagesModule.app.refresh() - }) + } else { + const errorModal = new OZONE.dialogs.ErrorDialog() + errorModal.content = ` +
Failed to delete applications.
+Please send a message to ${supportUser(true)}.
+ ` + errorModal.show() + } }) + confirmModal.focusButton = "cancel" confirmModal.show() } -function shouldShowDeleteButtons(hash) { - return hash === "" || hash.indexOf("inbox") !== -1 +/** + * @callback deleteMessagesBatches_beforeBatch + * @param {Number} batchIndex + * @param {Number} batchCount + * @param {Number} batchSize + * @return {Promise+ Delete applications userscript by ${supportUser()} +
+ + ` - const buttonLocation = document.getElementById("message-area").parentElement - buttonLocation.prepend(deleteButtonsContainer) + document + .getElementById("message-area") + .parentElement.prepend(deleteButtonsContainer) + document + .getElementById("delete-buttons") + .append(deleteRecentButton, deleteAllButton) + + // Detect clicks to messages and inbox tabs and hide/show buttons as appropriate + addEventListener("click", () => + setTimeout(() => { + deleteButtonsContainer.style.display = shouldShowDeleteButtons() + ? "flex" + : "none" + }, 500) + ) })() - -// Detect clicks to messages and inbox tabs and hide/show buttons as appropriate -addEventListener("click", () => { - setTimeout(() => { - toggleDeleteButtons() - }, 500) -}) diff --git a/screenshot.png b/screenshot.png index a9cb722..b773393 100644 Binary files a/screenshot.png and b/screenshot.png differ