NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript== // @name SW_Enhancer // @version 0.94.3 // @author Nina Lanyon // @grant GM_getValue // @grant GM_setValue // @grant GM_log // @require https://openuserjs.org/src/libs/sizzle/GM_config.js // @include https://similarworlds.com/* // @updateURL https://openuserjs.org/meta/ninalanyon/SW_Enhancer.meta.js // @downloadURL https://openuserjs.org/install/ninalanyon/SW_Enhancer.user.js // @copyright 2021, Nina Lanyon (https://openuserjs.org/users/ninalanyon) // @license MIT // ==/UserScript== /* Changes: - 0.94.3 Added let to for statements. - 0.94.2 Removed some logging that was reading properties before they existed. - 0.94.1 SW implemented scrolling of the highlighted respons into view so I have disabled my function. - 0.93.4 SW changed the name of the visibility element. - 0.93.3 SW changed the classname of active notifications causing hiding of inactive notifications to fail. Fixed. - 0.93.2 Fixed bug in hideUnreadableReplyAndThread which hides the comments replying to a deleted comment. - 0.93.1 Fixed bug in SetVisibility using window.eval to call and SW function. - 0.93.0 In progress: automatically follow people when responding. - 0.92.0 Hide read mail in https://similarworlds.com/messages - 0.90.3 Fixed the function that hides inactive notifications to hide content box divs when all of the content is hidden. - 0.90.1 Ignore mutations caused by the leechblock addon. - 0.90 Added option to show only unread notifications - 0.82 Added option to hide the Add downvote reaction button. - 0.8 Fixed error in function that hides the motivational quotes, it was hiding other elements as well. - 0.7 First published version. */ /* ---------------- Define the configuration dialog. */ (async function () { 'use strict'; // A few bits of shorthand function getElementById(id) { return document.getElementById(id); } function getElementsByClassName(name) { return document.getElementsByClassName(name); } function createElement(elementType) { return document.createElement(elementType); } // Name some of the fields, field types, etc. to assist in // preventing typos. const SExpandCollapsedPosts = 'expandCollapsedPosts'; const SExpandCollapsedReplies = 'expandCollapsedReplies'; const SHideUnreadableRepliesAndThread = 'hideUnreadableRepliesAndThread'; const SHideUnwantedPostsInFeed = 'hideUnwantedPostsInFeed'; const SMatchUnwantedPostsInFeed = 'matchUnwantedPostsInFeed'; const SIgnoreCaseWhenMatching = "ignoreCaseWhenMatching"; const SHidePosQuotes = "hidePosQuotes"; const SEnableSWEnhancer = "enableSWEnhancer"; const SHideSWReactionsNeg = "hideSWReactionsNeg"; const SShowOnlyUnreadNotifications = "showOnlyUnreadNotifications"; const SShowOnlyUnreadMail = "showOnlyUnreadMail"; const SSetVisibility = "setVisibility"; const VVisibilityDoNotSet = "Do not set"; const VVisibilityLimited = "Limited"; const VVisibilityPublic = "Public"; const VVisibilityPrivate = "Private"; function makeConfiguration() { console.log("a makeConfiguration"); const TCheckBox = "checkbox"; const TTextArea = "textarea"; const TRadio = "radio"; const KLabel = "label"; const KType = "type" const KDefault = "default"; const KTitle = "title"; const KSection = "section"; const KRows = "rows"; const KCols = "cols"; const KOptions = "options"; console.log("SW m"); var fieldDefs = { [SExpandCollapsedPosts]: { [KSection]: ["Hiding and revealing parts of a post"], [KLabel]: "Expand collapsed posts immediately", [KTitle]: "Pictures in posts are hidden by default, ticking this box makes them show always.", [KType]: TCheckBox, [KDefault]: false }, [SExpandCollapsedReplies]: { [KLabel]: "Expand collapsed replies immediately", [KTitle]: "Replies with sensitive content are collapsed by default on SW, ticking this box saves you the bother of clicking each one yourself.", [KType]: TCheckBox, [KDefault]: false }, [SHideUnreadableRepliesAndThread]: { [KLabel]: "Hide any replies that are marked as deleted together with all the replies to them", [KType]: TCheckBox, [KTitle]: "Once a reply has been deleted replies often become unfathomable and any you can't reply to them. Ticking this box hides the deleted item and all the replies to it.", [KDefault]: false }, [SHideUnwantedPostsInFeed]: { [KSection]: ["Feed related settings"], [KLabel]: 'Hide posts in the feed that match a regular expression', [KType]: TCheckBox, [KTitle]: "A simple example would be 'spank|diaper|poop' (without the quotation marks. This would hide any feed items that contain any of those words.", [KDefault]: false }, [SMatchUnwantedPostsInFeed]: { [KLabel]: "Enter terms below separated by a pipe symbol: |. " + "If any of these terms appear in post extracts or post titles in the feed then the post that contains them will be hidden. " + "Note that this applies to the posts and the comments.", [KType]: TTextArea, [KTitle]: "A simple example would be 'spank|diaper|poop' (without the quotation marks. This would hide any feed items that contain any of those words.", [KDefault]: "", [KCols]: 60, [KRows]: 4 }, [SIgnoreCaseWhenMatching]: { [KLabel]: "Case insensitive matching.", [KType]: TCheckBox, [KTitle]: "For instance match all of these: Spank, SPANK, spank", [KDefault]: true }, [SEnableSWEnhancer]: { [KSection]: ["Miscellaneous settings"], [KLabel]: 'Enable this tool', [KType]: TCheckBox, [KTitle]: "Disabling still allows you to change the settings.", [KDefault]: true }, [SHidePosQuotes]: { [KLabel]: 'Hide the silly motivational quotes', [KType]: TCheckBox, [KTitle]: "I find them irritating, I would rather that they weren't there.", [KDefault]: false }, [SHideSWReactionsNeg]: { [KLabel]: 'Hide the Downvote button in the Add Reaction popup', [KType]: TCheckBox, [KTitle]: "Makes it impossible to accidentally downvote and removes the irritating thing from my sight!", [KDefault]: true }, [SShowOnlyUnreadNotifications]: { [KLabel]: "Hide notifications that you have already clicked on.", [KType]: TCheckBox, [KTitle]: "Makes it harder to forget a notification.", [KDefault]: true }, [SShowOnlyUnreadMail]: { [KLabel]: "Hide mail that you have already clicked on.", [KType]: TCheckBox, [KTitle]: "Makes it harder to forget a message.", [KDefault]: true }, [SSetVisibility]: { [KLabel]: 'Post visibility', [KType]: TRadio, [KOptions]: [VVisibilityDoNotSet, VVisibilityPublic, VVisibilityLimited, VVisibilityPrivate], [KTitle]: "When creating a new post or editing one set the visibility.", [KDefault]: VVisibilityDoNotSet } }; GM_config.init({ id: 'SWEnhancerConfig', title: 'SW Enhancer Settings', fields: fieldDefs, }); console.log("SW bp "); //console.log("b " + GM_config.get(SExpandCollapsedPosts)); } makeConfiguration(); console.log("cp "); //console.log("c " + GM_config.get(SExpandCollapsedPosts)); /*try { console.log("d " + GM_config.get(SExpandCollapsedReplies)); } catch (error) { console.error(error); } */ function showConfiguration() { GM_config.open(); } console.log("SW F"); let menu = getElementById("sw-header-profile-drop"); if (menu != null) { let sep = createElement("div"); sep.className = "header-dropsep"; menu.appendChild(sep); console.log("SW G"); let item = createElement("div"); menu.appendChild(item); let a = createElement("a"); a.className = "opt"; a.onclick = function () { showConfiguration(); }; console.log("SW E "); a.innerHTML = '<i class="sw-icon sw-i-edit-small"></i>User Settings'; item.appendChild(a); } // See // https://plainjs.com/javascript/styles/get-the-position-of-an-element-relative-to-the-document-24/ function offset(el) { const rect = el.getBoundingClientRect(), scrollLeft = window.pageXOffset || document.documentElement.scrollLeft, scrollTop = window.pageYOffset || document.documentElement.scrollTop; return { left: rect.left + scrollLeft, top: rect.top + scrollTop }; } var highlightedElementTop = 0; var allowScroll = true; var lastUrl = ""; var postEntryHeight = 0; var cancelScrollTimerId = 0; function cancelScroll() { allowScroll = false; } // Return false if we should not scroll the highlighted comment // into view. To determine this we check to see if the user has // expanded the text areas for data entry. function allowedToScroll() { console.log("SW e allowedToScroll: ", allowScroll); // SW has implemented this themselves so we should not do it as the two fight each other. allowScroll = false return allowScroll if (lastUrl !== document.location.href) { // SW sets the page url without 'navigating' to it. Make // sure we start again if we are on a new page. console.log("e url changed"); allowScroll = true; postEntryHeight = 0; lastUrl = document.location.href; } if (allowScroll) { // Check to see if we have entered the text entry box. If // so cancel scrolling. const textEntries = getElementsByClassName("form-textarea"); const textEntry = textEntries.item(0); const h = textEntry.clientHeight; console.log("f: ", h); if (postEntryHeight == 0) { console.log("g peh was 0"); postEntryHeight = h; } if (postEntryHeight != h) { console.log("h peh changed"); postEntryHeight = h; allowScroll = false; } } return allowScroll; } function scrollToHighlightedComment() { if (!allowedToScroll()) { return; } const divs = getElementsByClassName("cmt-hghlt"); console.log("i scrollToHighlightedComment: ", divs); if (divs.length !== 0) { let div = divs.item(0); let rect = offset(div); // Round to integer to avoid scrolling because of changes // in the umpteenth decimal place. let newTop = Math.round(rect.top); console.log("j highlightedElementTop: ", highlightedElementTop); console.log("k rect: ", rect.top); if (newTop != highlightedElementTop) { // position has changed. highlightedElementTop = newTop; divs.item(0).scrollIntoView({ behavior: 'smooth', block: "end", inline: "nearest" }); clearTimeOut(cancelScrollTimeoutId); cancelScrollimerId = setTimeout(cancelScroll, 1000) } } } function clickButtons(className) { console.log("SW f"); const divs = getElementsByClassName(className); for (let i = 0; i < divs.length; i++) { divs.item(i).getElementsByClassName("small-button").item(0).click(); } } function unBlurInMessages() { console.log("SW g"); const anchors = getElementsByClassName("small-button blur-btn active"); [].forEach.call(anchors, function (a) { if (a.style.display != "none") { a.click(); } }) } function clickStoryCompressed() { console.log("SW h"); const a = getElementById("story-compressed"); if (a !== null) { a.click(); } } const rootUrl = "https://similarworlds.com/"; const feedsUrls = [rootUrl, rootUrl + "top", rootUrl + "new" ]; function getParent(element, rel) { if (rel === 0) { return element; } else { return getParent(element.parentElement, rel - 1); } } function hideContentBoxIfContentsHidden(contentBox) { console.log("i"); let active = contentBox.getElementsByClassName("sw-notif-active"); if (active.length == 0) { contentBox.style.display = "none"; } } function hideItemsRelative(elements, parentRel) { "use strict"; console.log("k"); let contentBoxes = new Set(); try { for (let i = 0; i < elements.length; i++) { let element = elements[i]; let parent = getParent(element, parentRel); parent.style.display = "none"; contentBoxes.add(parent.parentElement); } } catch (ex) { alert('b ex: ' + ex.toString()); } contentBoxes.forEach(hideContentBoxIfContentsHidden); } function hideItems(className, parentRel, matcher) { "use strict"; console.log("SW j"); var elements = document.getElementsByClassName(className), element, text, i; try { for (let i = 0; i < elements.length; i++) { element = elements[i]; if (element) { text = element.textContent; if (typeof text !== 'undefined') { if (text.search(matcher) !== -1) { let parent = getParent(element, parentRel); parent.style.display = "none"; //parent.visibility = "hidden"; //element.style.display = "none"; //element.visibility = "hidden"; } } } } } catch (ex) { alert('b ex: ' + ex.toString()); } } function hideUnwantedPostsInFeed() { let matchSrc = GM_config.get(SMatchUnwantedPostsInFeed); let matchInsensitive = GM_config.get(SIgnoreCaseWhenMatching); let flags = matchInsensitive ? "i" : ""; let matcher = new RegExp(matchSrc, flags); hideItems("swcmnt", 3, matcher); hideItems("cmnt-post-prevw", 3, matcher); hideItems("pstbx-group", 2, matcher); hideItems("rctrpl-comment", 3, matcher); } function runClickButtons() { // click the view more replies button console.log("d "); //alert("runClickButtons"); if (GM_config.get(SExpandCollapsedReplies)) { clickButtons("nested-comments-getmore"); } clickButtons("cmtbx-showhdn"); unBlurInMessages(); console.log("e "); if (GM_config.get(SExpandCollapsedPosts)) { clickStoryCompressed(); } // TODO: reinstate when fixed: //scrollToHighlightedComment(); } function hideClass(className) { "use strict"; var elements = document.getElementsByClassName(className); console.log("SW l " + className + " " + elements.length); for (var i = 0; i < elements.length; i++) { let element = elements[i]; element.style.display = "none"; } } function hidePosQuotes() { "use strict"; console.log("SW p hidePosQuotes"); hideClass("pos-quote"); hideClass("sw-body-1-aa"); hideClass("pos-quote-in"); //hideClass("cmtbx-bx-body"); //hideClass("cmtbx-elem"); let e = getElementById("sw-right-side-aa"); e.style.display = "none"; } function hideSWReactionsNeg() { hideClass("sw-reactions neg"); } function hideOldNotifications() { let active = getElementsByClassName("sw-notif active"); if (active.length == 0) { // No active items. So we should not hide the inactive ones either. return; } let inactive = document.querySelectorAll('.sw-notif:not(.active)'); hideItemsRelative(inactive, 0); } function hideOldMail() { let active = document.querySelectorAll('.message-box:not(.read)'); if (active.length == 0) { // No active items. So we should not hide the inactive ones either. return; } hideClass('message-box read'); } function hideUnreadableReplyAndThread(element) { "use strict"; element.style.display = "none"; let next = element.nextElementSibling; if (next == null) { return; } next.style.display = "none"; /*let comslst = next.firstElementChild; let firstComment = comslst.firstElementChild; if (firstComment.classList.contains("cmt-nst")){ comslst.style.display = "none"; } */ } function hideUnreadableRepliesAndThread() { "use strict"; var elements = document.getElementsByClassName("comment-deleted"); console.log("SW sa " + elements.length); for (var i = 0; i < elements.length; i++) { hideUnreadableReplyAndThread(elements[i]); } } // function setVisibility(){ // let postseg_3 = getElementById("postseg-3"); // if (postseg_3 == null || postseg_3.style.display == "none"){ // // Not ready to be set yet or not on the right page. // return; // } // let newVisibility = GM_config.get(SSetVisibility); // let code = [VVisibilityDoNotSet, // VVisibilityPublic, // VVisibilityPrivate, // VVisibilityLimited].indexOf(newVisibility); // if (code == 0){ // return; // } // let visibilitySpan = getElementById("postentry-visible" + code); // if (visibilitySpan.style.display == "none"){ // window.eval("postEditVisibility('0', " + code + ", 'postentry-visible');"); // } // } function setVisibility() { var visibilitySpan; for (let code = 1; code <= 3; code++) { visibilitySpan = getElementById("postentry-visible" + code); if (visibilitySpan != null && visibilitySpan.style.display != "none") { break; } } if (visibilitySpan == null || visibilitySpan.style.display == "none") { // Not ready to be set yet or not on the right page. return; } let newVisibility = GM_config.get(SSetVisibility); let code = [VVisibilityDoNotSet, VVisibilityPublic, VVisibilityPrivate, VVisibilityLimited ].indexOf(newVisibility); if (code == 0) { return; } visibilitySpan = getElementById("postentry-visible" + code); if (visibilitySpan.style.display == "none") { window.eval("postEditVisibility('0', " + code + ", 'postentry-visible');"); } } function runFixUp() { console.log("SW q runFixUp"); //alert("SW a " + window.location.href); if (feedsUrls.includes(window.location.href)) { if (GM_config.get(SHideUnwantedPostsInFeed)) { hideUnwantedPostsInFeed(); } } else { runClickButtons(); } if (GM_config.get(SHidePosQuotes, false)) { hidePosQuotes(); } if (GM_config.get(SHideSWReactionsNeg, true)) { hideSWReactionsNeg(); } if (window.location.href === "https://similarworlds.com/notifications" && GM_config.get(SShowOnlyUnreadNotifications, true)) { hideOldNotifications(); } if (window.location.href === "https://similarworlds.com/messages" && GM_config.get(SShowOnlyUnreadMail, true)) { hideOldMail(); } if (GM_config.get(SHideUnreadableRepliesAndThread, true)) { hideUnreadableRepliesAndThread(); } setVisibility(); } function run() { console.log("SW n run"); //alert("SW B"); let timerId = 0; const observer = new MutationObserver((mutationList, observer) => { if (mutationList.length == 1 && mutationList[0].target.className == "leechblock-timer") { // Leech block places a countdown in the top left // corner which causes mutations that we aren't // interested in, just ignore them. return; } console.log("SW o observer", mutationList); // cancel the timeout if it is running so that we don't run // until the cascade of mutations has died down. clearTimeout(timerId); // click the buttons a little while after the last mutation event timerId = setTimeout(runFixUp, 100) }); // observe everything observer.observe( document.body, { childList: true, // observe direct children subtree: true, // and lower descendants too attributes: true } ); } console.log("SW D"); //alert("SW D "); //if (GM_config.get(SEnableSWEnhancer)) { if (true) { console.log("SW l"); //alert("SW C"); run(); } })()