// ==UserScript== // @name AO3: [Wrangling] View and Post Comments from the Bin // @namespace https://greasyfork.org/en/users/906106-escctrl // @description Loads a preview of top-level comments (such as translations) and lets you comment on the tag // @author escctrl // @version 2.5 // @match *://*.archiveofourown.org/tags/*/wrangle?* // @grant none // @require https://ajax.googleapis.com/ajax/libs/jquery/3.7.0/jquery.min.js // @require https://ajax.googleapis.com/ajax/libs/jqueryui/1.13.2/jquery-ui.min.js // @require https://cdnjs.cloudflare.com/ajax/libs/jqueryui-touch-punch/0.2.3/jquery.ui.touch-punch.min.js // @require https://update.greasyfork.icu/scripts/491888/1355841/Light%20or%20Dark.js // @license MIT // @downloadURL https://update.greasyfork.icu/scripts/532650/AO3%3A%20%5BWrangling%5D%20View%20and%20Post%20Comments%20from%20the%20Bin.user.js // @updateURL https://update.greasyfork.icu/scripts/532650/AO3%3A%20%5BWrangling%5D%20View%20and%20Post%20Comments%20from%20the%20Bin.meta.js // ==/UserScript== /* eslint-disable no-multi-spaces */ /* global jQuery, lightOrDark */ (function($) { 'use strict'; if ($('#wrangulator').length === 0) return; // bow out in an empty bin /***** INITIALIZE THE COMMENTS DIALOG *****/ let dlg = "#peekTopLevelCmt"; // prepare the HTML framework within the new dialog including the textarea $("#main").append(`

`); // adding the jQuery stylesheet to style the dialog, and fixing the interference of AO3's styling if(document.head.querySelector('link[href$="/jquery-ui.css"]') === null) { // if the background is dark, use the dark UI theme to match let dialogtheme = lightOrDark($('body').css('background-color')) == "dark" ? "dark-hive" : "base"; $("head").append(``); } $("head").append(``); // optimizing the size of the GUI in case it's a mobile device let dialogwidth = window.visualViewport.width; dialogwidth = dialogwidth > 700 ? 700 : dialogwidth * 0.9; let dialogheight = window.visualViewport.height; dialogheight = dialogheight * 0.7; $(dlg).dialog({ appendTo: "#main", modal: false, autoOpen: false, resizable: false, width: dialogwidth, maxHeight: dialogheight, title: "Tag Comment Threads", close: function() { // reset everything including the data-tagname attribute resetDialog(); $(dlg).find('textarea')[0].dataset.tagname = ""; $(dlg).data('openedOnTag', ""); } }); // we add a button the Manage header to load all comments on this page at once $('#wrangulator').find('thead th').filter(function(ix, el) { return $(this).text() === "Manage"; }) .append(`
`); /***** BUTTON EVENTS *****/ // when user clicks the "Load all Comments" button $('#wrangulator').on('click','#load-toplvlcmt', function(e) { e.preventDefault(); loadAllTopLevelComments(); }); // we co-opt the /comments link and instead of opening the page, we give a little additional dialog $('#wrangulator').on('click','a[href$="/comments"]', async function(e) { e.preventDefault(); let row = $(e.target).parents('tr')[0]; if (row.dataset.cmttoptevel === "unknown") { let resolved = await loadTopLevelComments(row); // if we have no data for this tag yet, we load it if (resolved === "failed") { // if XHR failed, we have probably encountered a 403 or 500 error. don't open the dialog alert('Top Level Comments could be loaded, we encountered errors trying to load the Comment page.'); return; } } $(dlg).find('textarea').prop('value', ''); // empty the textbox content from whatever was there before viewTopLevelComments(e.currentTarget); // then we write out the dialog }); // not a button event, but if the window resizes the dialog would move off of the screen $(window).on('resize', function(e) { // optimizing the size of the GUI in case it's a mobile device let dialogwidth = window.visualViewport.width; dialogwidth = dialogwidth > 700 ? 700 : dialogwidth * 0.9; let dialogheight = window.visualViewport.height; dialogheight = dialogheight * 0.7; let target = $(dlg).dialog("option", "position").of; // find the current target button // reposition and resize the dialog $(dlg).dialog("option", "position", { my: "left top", at: "left bottom", of: target } ) .dialog("option", "width", dialogwidth) .dialog("option", "maxHeight", dialogheight); }); /***** RETRIEVE TOP-LEVEL COMMENTS *****/ // on pageload, silently loop over all tags on the page // check if we already have the tag stored // grab the user's pseuds from storage let pseuds = JSON.parse(sessionStorage.getItem("cmt_pseud") || null); $(dlg).find('#pseud-toplvlcmt').html(pseuds); // after a couple of seconds we can be reasonably sure other scripts would have added their comments buttons let timeout = setTimeout(() => { let cmtButtonExists = $('#wrangulator').find('a[href$="/comments"]').length; if (!cmtButtonExists) console.log('added Comments buttons after 2 sec because none were present from other scripts. if your script is slower, increase the wait period.'); let rows = $('#wrangulator').find('tbody tr').toArray(); for (let row of rows) { if (!cmtButtonExists) { // failsafe: if there are no comment buttons from another script, add them let edit = $(row).find('a[href$="/edit"]').last(); $(edit).parent().after(`
  • ${ $(edit).css('font-family').includes("FontAwesome") === true ? "" : "Comments" }
  • `); } // retrieve what we already have in storage for this tag let tagname = $(row).find('th label').text(); let cmtTopLevel = JSON.parse(sessionStorage.getItem("cmt_" + tagname) || null); if (cmtTopLevel !== null) { // if we found something in storage cmtTopLevel = new Map(cmtTopLevel); // and then we should put it somewhere so we know not to ask again on click row.dataset.cmttoptevel = "stored"; } else row.dataset.cmttoptevel = "unknown"; } }, 2 * 1000); // << WAIT PERIOD FOR OTHER SCRIPTS TO HAVE ADDED COMMENT BUTTONS. increase the standard 2 to 5 if your script is slower async function loadAllTopLevelComments() { $('#load-toplvlcmt').attr('disabled', true) // stop button from being clicked again .find('.spin').css('display', 'inline-block'); // loading indicator let rows = $('#wrangulator').find('tbody tr').toArray(); let rowsToDo = $(rows).filter( function() { return this.dataset.cmttoptevel === "unknown"; } ).toArray(); // only worry about not stored items // when clicking the button, loop over all tags on the page for (let row of rowsToDo) { let resolved = await loadTopLevelComments(row); // grab the top level comments from the page if (resolved === "loaded") await waitforXSeconds(2); // if XHR succeeded, creates a x-seconds wait period between function calls else { // if XHR failed, we have probably encountered a 403 or 500 error. don't try more pages alert('Not all Top Level Comments could be loaded, we encountered errors trying to load the Comment pages.'); break; } } $('#load-toplvlcmt').text('All Comments Loaded') // change text .find('.spin').css('display', 'none'); // remove loading indicator } function waitforXSeconds(x) { return new Promise((resolve) => { setTimeout(() => { resolve(""); }, x * 1000); }); } /***** COMMON FUNCTION FOR BACKGROUND PAGELOADS *****/ function loadTopLevelComments(row) { return new Promise((resolve) => { let target = $(row).find('[href$="/comments"]')[0]; let tagname = $(row).find('th label').text(); let xhr = $.ajax({ url: $(target).prop('href'), type: 'GET' }) .fail(function(xhr, status) { console.warn(`Top level comments for ${tagname} could not be loaded due to response error:`, status); resolve("failed"); }).done(function(response) { // in case we're served a code:200 page that doesn't actually contain the comments page, we quit if ($(response).find('#feedback').length === 0) { console.warn(`Top level comments for ${tagname} could not be loaded because response didn't contain the #feedback`); resolve("failed"); return; } // grab this user's possible pseuds and store it in session pseuds = $(response).find('#add_comment h4.heading')[0].outerHTML; sessionStorage.setItem("cmt_pseud", JSON.stringify(pseuds)); let cmtStorage = parseTopLevelComments(response); sessionStorage.setItem("cmt_" + tagname, JSON.stringify([...cmtStorage])); // store all comments in session row.dataset.cmttoptevel = "stored"; // set this tag from unknown to stored, so we don't try to load it again resolve("loaded"); }); }); } function parseTopLevelComments(response) { // grab top level comments let cmtTopLevel = $(response).find('#comments_placeholder > ol.thread > li.comment').toArray(); // we'll store the comments in a Map to ensure insertion order while being able to later use an object key to reference the item let cmtStorage = new Map(); for (let [i, v] of cmtTopLevel.entries()) { let commentID = $(v).find('li[id^="edit_comment_link_"')?.prop('id') || ""; // grab the CSS ID off the Edit button (our comment if it exists) commentID = commentID === "" ? 'c'+i : commentID.match(/\d+/)[0]; // if there's no ID, just count to 20, otherwise grab the commentID let by = $(v).find('.byline a').text(); // name of the user who posted the comment let content = $(v).find('.userstuff').html().trim(); // content of the post cmtStorage.set(commentID, [by, content]); // put the comment into our storage } return cmtStorage; } /***** DISPLAY THE COMMENTS DIALOG *****/ function viewTopLevelComments(target) { let tagname = $(target).parents('tr').first().find('th label').text(); let taglink = $(target).prop('href'); $(dlg).dialog("option", "title", tagname ); // dialog title shows tagname for which it was opened $(dlg).data('openedOnTag', target); // store button from which this dialog was opened // create a link to the plain Comments page let iconExternalLink = ``; iconExternalLink = `Open Comment Page ${iconExternalLink}`; $('#toplvlcmt-pagelink').html(iconExternalLink); // create the preview of the first 20 comments let comments = new Map(JSON.parse(sessionStorage.getItem("cmt_" + tagname))); let content = (comments.size === 0) ? `
    There are no comments on this tag yet.
    ` : ""; for (let [id, comment] of comments.entries()) { let editButton = !id.startsWith('c') ? ` ` : ""; content += `

    by ${ comment[0] }${ editButton }

    ${ comment[1] }
    `; } $(dlg).find('#toplvlcmt-placeholder').html(content); // write comments to the dialog // a textbox to leave a new comment - works with the "Comment Formatting & Preview" script let cmtNew = $(dlg).find('#add-toplvlcmt textarea')[0]; cmtNew.dataset.tagname = encodeURIComponent(tagname); cmtNew.dataset.commentid = ""; $(dlg).find('#pseud-toplvlcmt').html(pseuds); $(dlg).dialog("option", "position", { my: "left top", at: "left bottom", of: target } ) // position the dialog at the clicked button .dialog('open'); // finally, open the dialog } /***** SUBMIT A NEW COMMENT *****/ $('#main').on('click', `${dlg} button.submitComment`, function(e) { e.preventDefault(); $(dlg).find('.submitComment').attr('disabled', true) // stop button from being clicked again .find('.spin').css('display', 'inline-block').html("(submitting)"); // loading indicator let tagname = decodeURIComponent($(dlg).find('textarea').attr('data-tagname')); let target = $(dlg).data('openedOnTag'); // collect various input for commenting let cmtData = new FormData(); cmtData.set('comment[comment_content]', $(dlg).find('textarea').prop('value')); cmtData.set('comment[pseud_id]', $(dlg).find('[name="comment[pseud_id]"]').prop('value')); // either a hidden or a